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);
60 Create a new transaction.
62 This routine should _never_ be called anything other Than RT::Ticket. It should not be called
63 from client code. Ever. Not ever. If you do this, we will hunt you down. and break your kneecaps.
64 Then the unpleasant stuff will start.
66 TODO: Document what gets passed to this
86 #if we didn't specify a ticket, we need to bail
87 unless ( $args{'Ticket'} ) {
88 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify a ticket id"));
93 #lets create our transaction
94 my %params = (Ticket => $args{'Ticket'},
95 Type => $args{'Type'},
96 Data => $args{'Data'},
97 Field => $args{'Field'},
98 OldValue => $args{'OldValue'},
99 NewValue => $args{'NewValue'},
100 Created => $args{'Created'}
103 # Parameters passed in during an import that we probably don't want to touch, otherwise
104 foreach my $attr qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy) {
105 $params{$attr} = $args{$attr} if ($args{$attr});
108 my $id = $self->SUPER::Create(%params);
110 $self->_Attach( $args{'MIMEObj'} )
111 if defined $args{'MIMEObj'};
113 #Provide a way to turn off scrips if we need to
114 if ( $args{'ActivateScrips'} ) {
116 #We're really going to need a non-acled ticket for the scrips to work
117 my $TicketAsSystem = RT::Ticket->new($RT::SystemUser);
118 $TicketAsSystem->Load( $args{'Ticket'} )
119 || $RT::Logger->err("$self couldn't load ticket $args{'Ticket'}\n");
121 my $TransAsSystem = RT::Transaction->new($RT::SystemUser);
122 $TransAsSystem->Load( $self->id )
124 "$self couldn't load a copy of itself as superuser\n");
125 # {{{ Deal with Scrips
128 my $PossibleScrips = RT::Scrips->new($RT::SystemUser);
130 $PossibleScrips->LimitToQueue( $TicketAsSystem->QueueObj->Id )
131 ; #Limit it to $Ticket->QueueObj->Id
132 $PossibleScrips->LimitToGlobal()
133 unless $TicketAsSystem->QueueObj->Disabled; # or to "global"
136 $PossibleScrips->Limit(FIELD => "Stage", VALUE => "TransactionCreate");
139 my $ConditionsAlias = $PossibleScrips->NewAlias('ScripConditions');
141 $PossibleScrips->Join(
143 FIELD1 => 'ScripCondition',
144 ALIAS2 => $ConditionsAlias,
148 #We only want things where the scrip applies to this sort of transaction
149 $PossibleScrips->Limit(
150 ALIAS => $ConditionsAlias,
151 FIELD => 'ApplicableTransTypes',
153 VALUE => $args{'Type'},
154 ENTRYAGGREGATOR => 'OR',
157 # Or where the scrip applies to any transaction
158 $PossibleScrips->Limit(
159 ALIAS => $ConditionsAlias,
160 FIELD => 'ApplicableTransTypes',
163 ENTRYAGGREGATOR => 'OR',
166 #Iterate through each script and check it's applicability.
168 while ( my $Scrip = $PossibleScrips->Next() ) {
169 $Scrip->Apply (TicketObj => $TicketAsSystem,
170 TransactionObj => $TransAsSystem);
177 return ( $id, $self->loc("Transaction Created") );
187 $self->loc('Deleting this object could break referential integrity') );
192 # {{{ Routines dealing with Attachments
198 Returns the RT::Attachments Object which contains the "top-level"object
199 attachment for this transaction
207 if ( !defined( $self->{'message'} ) ) {
209 $self->{'message'} = new RT::Attachments( $self->CurrentUser );
210 $self->{'message'}->Limit(
211 FIELD => 'TransactionId',
215 $self->{'message'}->ChildrenOf(0);
217 return ( $self->{'message'} );
224 =head2 Content PARAMHASH
226 If this transaction has attached mime objects, returns the first text/plain part.
227 Otherwise, returns undef.
229 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
230 at $args{'Wrap'}. $args{'Wrap'} defaults to 70.
244 my $content_obj = $self->ContentObj;
246 $content = $content_obj->Content;
249 # If all else fails, return a message that we couldn't find any content
251 $content = $self->loc('This transaction appears to have no content');
254 if ( $args{'Quote'} ) {
256 # Remove quoted signature.
257 $content =~ s/\n-- \n(.*)$//s;
259 # What's the longest line like?
261 foreach ( split ( /\n/, $content ) ) {
262 $max = length if ( length > $max );
266 require Text::Wrapper;
267 my $wrapper = new Text::Wrapper(
268 columns => $args{'Wrap'},
269 body_start => ( $max > 70 * 3 ? ' ' : '' ),
272 $content = $wrapper->wrap($content);
276 . $self->CreatorObj->Name() . ' - '
277 . $self->CreatedAsString() . "]:\n\n" . $content . "\n\n";
278 $content =~ s/^/> /gm;
291 Returns the RT::Attachment object which contains the content for this Transaction
301 # If we don\'t have any content, return undef now.
302 unless ( $self->Attachments->First ) {
306 # Get the set of toplevel attachments to this transaction.
307 my $Attachment = $self->Attachments->First();
309 # If it's a message or a plain part, just return the
311 if ( $Attachment->ContentType() =~ '^(text/plain$|message/)' ) {
312 return ($Attachment);
315 # If it's a multipart object, first try returning the first
318 elsif ( $Attachment->ContentType() =~ '^multipart/' ) {
319 my $plain_parts = $Attachment->Children();
320 $plain_parts->ContentType( VALUE => 'text/plain' );
322 # If we actully found a part, return its content
323 if ( $plain_parts->First && $plain_parts->First->Content ne '' ) {
324 return ( $plain_parts->First );
327 # If that fails, return the first text/plain or message/ part
328 # which has some content.
331 my $all_parts = $Attachment->Children();
332 while ( my $part = $all_parts->Next ) {
333 if (( $part->ContentType() =~ '^(text/plain$|message/)' ) && $part->Content() ) {
341 # We found no content. suck
351 If this transaction has attached mime objects, returns the first one's subject
352 Otherwise, returns null
358 if ( $self->Attachments->First ) {
359 return ( $self->Attachments->First->Subject );
368 # {{{ sub Attachments
372 Returns all the RT::Attachment objects which are attached
373 to this transaction. Takes an optional parameter, which is
374 a ContentType that Attachments should be restricted to.
381 unless ( $self->{'attachments'} ) {
382 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
384 #If it's a comment, return an empty object if they don't have the right to see it
385 if ( $self->Type eq 'Comment' ) {
386 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
387 return ( $self->{'attachments'} );
391 #if they ain't got rights to see, return an empty object
393 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
394 return ( $self->{'attachments'} );
398 $self->{'attachments'}->Limit( FIELD => 'TransactionId',
399 VALUE => $self->Id );
401 # Get the self->{'attachments'} in the order they're put into
402 # the database. Arguably, we should be returning a tree
403 # of self->{'attachments'}, not a set...but no current app seems to need
406 $self->{'attachments'}->OrderBy( ALIAS => 'main',
411 return ( $self->{'attachments'} );
421 A private method used to attach a mime object to this transaction.
427 my $MIMEObject = shift;
429 if ( !defined($MIMEObject) ) {
431 "$self _Attach: We can't attach a mime object if you don't give us one.\n"
433 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
436 my $Attachment = new RT::Attachment( $self->CurrentUser );
438 TransactionId => $self->Id,
439 Attachment => $MIMEObject
441 return ( $Attachment, $self->loc("Attachment created") );
449 # {{{ Routines dealing with Transaction Attributes
451 # {{{ sub Description
455 Returns a text string which describes this transaction
463 #If it's a comment, we need to be extra special careful
464 if ( $self->__Value('Type') eq 'Comment' ) {
465 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
466 return ( $self->loc("Permission Denied") );
470 #if they ain't got rights to see, don't let em
472 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
473 return ($self->loc("Permission Denied") );
477 if ( !defined( $self->Type ) ) {
478 return ( $self->loc("No transaction type specified"));
481 return ( $self->loc("[_1] by [_2]",$self->BriefDescription , $self->CreatorObj->Name ));
486 # {{{ sub BriefDescription
488 =head2 BriefDescription
490 Returns a text string which briefly describes this transaction
494 sub BriefDescription {
499 #If it's a comment, we need to be extra special careful
500 if ( $self->__Value('Type') eq 'Comment' ) {
501 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
502 return ( $self->loc("Permission Denied") );
506 #if they ain't got rights to see, don't let em
508 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
509 return ( $self->loc("Permission Denied") );
513 my $type = $self->Type; #cache this, rather than calling it 30 times
515 if ( !defined( $type ) ) {
516 return $self->loc("No transaction type specified");
519 if ( $type eq 'Create' ) {
520 return ($self->loc("Ticket created"));
522 elsif ( $type =~ /Status/ ) {
523 if ( $self->Field eq 'Status' ) {
524 if ( $self->NewValue eq 'deleted' ) {
525 return ($self->loc("Ticket deleted"));
528 return ( $self->loc("Status changed from [_1] to [_2]", $self->loc($self->OldValue), $self->loc($self->NewValue) ));
534 my $no_value = $self->loc("(no value)");
535 return ( $self->loc( "[_1] changed from [_2] to [_3]", $self->Field , ( $self->OldValue || $no_value ) , $self->NewValue ));
538 if ( $type eq 'Correspond' ) {
539 return $self->loc("Correspondence added");
542 elsif ( $type eq 'Comment' ) {
543 return $self->loc("Comments added");
546 elsif ( $type eq 'CustomField' ) {
548 my $field = $self->loc('CustomField');
550 if ( $self->Field ) {
551 my $cf = RT::CustomField->new( $self->CurrentUser );
552 $cf->Load( $self->Field );
553 $field = $cf->Name();
556 if ( $self->OldValue eq '' ) {
557 return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
559 elsif ( $self->NewValue eq '' ) {
560 return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
564 return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
568 elsif ( $type eq 'Untake' ) {
569 return $self->loc("Untaken");
572 elsif ( $type eq "Take" ) {
573 return $self->loc("Taken");
576 elsif ( $type eq "Force" ) {
577 my $Old = RT::User->new( $self->CurrentUser );
578 $Old->Load( $self->OldValue );
579 my $New = RT::User->new( $self->CurrentUser );
580 $New->Load( $self->NewValue );
582 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
584 elsif ( $type eq "Steal" ) {
585 my $Old = RT::User->new( $self->CurrentUser );
586 $Old->Load( $self->OldValue );
587 return $self->loc("Stolen from [_1] ", $Old->Name);
590 elsif ( $type eq "Give" ) {
591 my $New = RT::User->new( $self->CurrentUser );
592 $New->Load( $self->NewValue );
593 return $self->loc( "Given to [_1]", $New->Name );
596 elsif ( $type eq 'AddWatcher' ) {
597 my $principal = RT::Principal->new($self->CurrentUser);
598 $principal->Load($self->NewValue);
599 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
602 elsif ( $type eq 'DelWatcher' ) {
603 my $principal = RT::Principal->new($self->CurrentUser);
604 $principal->Load($self->OldValue);
605 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
608 elsif ( $type eq 'Subject' ) {
609 return $self->loc( "Subject changed to [_1]", $self->Data );
612 elsif ( $type eq 'AddLink' ) {
614 if ($self->NewValue) {
615 my $URI = RT::URI->new($self->CurrentUser);
616 $URI->FromURI($self->NewValue);
617 if ($URI->Resolver) {
618 $value = $URI->Resolver->AsString;
620 $value = $self->NewValue;
623 if ($self->Field eq 'DependsOn') {
624 return $self->loc("Dependency on [_1] added",$value);
625 } elsif ($self->Field eq 'DependedOnBy') {
626 return $self->loc("Dependency by [_1] added",$value);
628 } elsif ($self->Field eq 'RefersTo') {
629 return $self->loc("Reference to [_1] added",$value);
630 } elsif ($self->Field eq 'ReferredToBy') {
631 return $self->loc("Reference by [_1] added",$value);
632 } elsif ($self->Field eq 'MemberOf') {
633 return $self->loc("Membership in [_1] added",$value);
634 } elsif ($self->Field eq 'HasMember') {
635 return $self->loc("Member [_1] added",$value);
637 return ( $self->Data );
640 elsif ( $type eq 'DeleteLink' ) {
642 if ($self->OldValue) {
643 my $URI = RT::URI->new($self->CurrentUser);
644 $URI->FromURI($self->OldValue);
645 if ($URI->Resolver) {
646 $value = $URI->Resolver->AsString;
648 $value = $self->OldValue;
652 if ($self->Field eq 'DependsOn') {
653 return $self->loc("Dependency on [_1] deleted",$value);
654 } elsif ($self->Field eq 'DependedOnBy') {
655 return $self->loc("Dependency by [_1] deleted",$value);
657 } elsif ($self->Field eq 'RefersTo') {
658 return $self->loc("Reference to [_1] deleted",$value);
659 } elsif ($self->Field eq 'ReferredToBy') {
660 return $self->loc("Reference by [_1] deleted",$value);
661 } elsif ($self->Field eq 'MemberOf') {
662 return $self->loc("Membership in [_1] deleted",$value);
663 } elsif ($self->Field eq 'HasMember') {
664 return $self->loc("Member [_1] deleted",$value);
666 return ( $self->Data );
669 elsif ( $type eq 'Set' ) {
670 if ( $self->Field eq 'Queue' ) {
671 my $q1 = new RT::Queue( $self->CurrentUser );
672 $q1->Load( $self->OldValue );
673 my $q2 = new RT::Queue( $self->CurrentUser );
674 $q2->Load( $self->NewValue );
675 return $self->loc("[_1] changed from [_2] to [_3]", $self->Field , $q1->Name , $q2->Name);
678 # Write the date/time change at local time:
679 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
680 my $t1 = new RT::Date($self->CurrentUser);
681 $t1->Set(Format => 'ISO', Value => $self->NewValue);
682 my $t2 = new RT::Date($self->CurrentUser);
683 $t2->Set(Format => 'ISO', Value => $self->OldValue);
684 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, $t2->AsString, $t1->AsString );
687 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, $self->OldValue, $self->NewValue );
690 elsif ( $type eq 'PurgeTransaction' ) {
691 return $self->loc("Transaction [_1] purged", $self->Data);
694 return $self->loc( "Default: [_1]/[_2] changed from [_3] to [_4]", $type, $self->Field, $self->OldValue, $self->NewValue );
701 # {{{ Utility methods
707 Returns true if the creator of the transaction is a requestor of the ticket.
708 Returns false otherwise
714 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
721 sub _ClassAccessible {
724 id => { read => 1, type => 'int(11)', default => '' },
726 { read => 1, write => 1, type => 'int(11)', default => '' },
728 { read => 1, public => 1, type => 'int(11)', default => '' },
729 TimeTaken => { read => 1, type => 'int(11)', default => '' },
730 Type => { read => 1, type => 'varchar(20)', default => '' },
731 Field => { read => 1, type => 'varchar(40)', default => '' },
732 OldValue => { read => 1, type => 'varchar(255)', default => '' },
733 NewValue => { read => 1, type => 'varchar(255)', default => '' },
734 Data => { read => 1, type => 'varchar(100)', default => '' },
735 Creator => { read => 1, auto => 1, type => 'int(11)', default => '' },
737 { read => 1, auto => 1, type => 'datetime', default => '' },
750 return ( 0, $self->loc('Transactions are immutable') );
759 Takes the name of a table column.
760 Returns its value as a string, if the user passes an ACL check
769 #if the field is public, return it.
770 if ( $self->_Accessible( $field, 'public' ) ) {
771 return ( $self->__Value($field) );
775 #If it's a comment, we need to be extra special careful
776 if ( $self->__Value('Type') eq 'Comment' ) {
777 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
782 #if they ain't got rights to see, don't let em
784 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
789 return ( $self->__Value($field) );
795 # {{{ sub CurrentUserHasRight
797 =head2 CurrentUserHasRight RIGHT
799 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
804 sub CurrentUserHasRight {
808 $self->CurrentUser->HasRight(
810 Object => $self->TicketObj