1 # $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Transaction.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
2 # Copyright 1999-2001 Jesse Vincent <jesse@fsck.com>
3 # Released under the terms of the GNU Public License
7 RT::Transaction - RT\'s transaction object
17 Each RT::Transaction describes an atomic change to a ticket object
18 or an update to an RT::Ticket object.
19 It can have arbitrary MIME attachments.
26 ok(require RT::TestHarness);
27 ok(require RT::Transaction);
33 package RT::Transaction;
43 $self->{'table'} = "Transactions";
44 return ($self->SUPER::_Init(@_));
53 Create a new transaction.
55 This routine should _never_ be called anything other Than RT::Ticket. It should not be called
56 from client code. Ever. Not ever. If you do this, we will hunt you down. and break your kneecaps.
57 Then the unpleasant stuff will start.
59 TODO: Document what gets passed to this
65 my %args = ( id => undef,
78 #if we didn't specify a ticket, we need to bail
79 unless ( $args{'Ticket'} ) {
80 return(0, "RT::Transaction->Create couldn't, as you didn't specify a ticket id");
83 #lets create our transaction
84 my $id = $self->SUPER::Create(Ticket => $args{'Ticket'},
85 TimeTaken => $args{'TimeTaken'},
86 Type => $args{'Type'},
87 Data => $args{'Data'},
88 Field => $args{'Field'},
89 OldValue => $args{'OldValue'},
90 NewValue => $args{'NewValue'},
91 Created => $args{'Created'}
94 $self->_Attach($args{'MIMEObj'})
95 if defined $args{'MIMEObj'};
97 #Provide a way to turn off scrips if we need to
98 if ($args{'ActivateScrips'}) {
100 #We're really going to need a non-acled ticket for the scrips to work
101 my $TicketAsSystem = RT::Ticket->new($RT::SystemUser);
102 $TicketAsSystem->Load($args{'Ticket'}) ||
103 $RT::Logger->err("$self couldn't load ticket $args{'Ticket'}\n");
105 my $TransAsSystem = RT::Transaction->new($RT::SystemUser);
106 $TransAsSystem->Load($self->id) ||
107 $RT::Logger->err("$self couldn't load a copy of itself as superuser\n");
109 # {{{ Deal with Scrips
111 #Load a scripscopes object
113 my $PossibleScrips = RT::Scrips->new($RT::SystemUser);
115 $PossibleScrips->LimitToQueue($TicketAsSystem->QueueObj->Id); #Limit it to $Ticket->QueueObj->Id
116 $PossibleScrips->LimitToGlobal(); # or to "global"
117 my $ConditionsAlias = $PossibleScrips->NewAlias('ScripConditions');
119 $PossibleScrips->Join(ALIAS1 => 'main', FIELD1 => 'ScripCondition',
120 ALIAS2 => $ConditionsAlias, FIELD2=> 'id');
123 #We only want things where the scrip applies to this sort of transaction
124 $PossibleScrips->Limit(ALIAS=> $ConditionsAlias,
125 FIELD=>'ApplicableTransTypes',
127 VALUE => $args{'Type'},
128 ENTRYAGGREGATOR => 'OR',
131 # Or where the scrip applies to any transaction
132 $PossibleScrips->Limit(ALIAS=> $ConditionsAlias,
133 FIELD=>'ApplicableTransTypes',
136 ENTRYAGGREGATOR => 'OR',
139 #Iterate through each script and check it's applicability.
141 while (my $Scrip = $PossibleScrips->Next()) {
143 #TODO: properly deal with errors raised in this scrip loop
145 #$RT::Logger->debug("$self now dealing with ".$Scrip->Id. "\n");
147 local $SIG{__DIE__} = sub { $RT::Logger->error($_[0])};
150 #Load the scrip's Condition object
151 $Scrip->ConditionObj->LoadCondition(TicketObj => $TicketAsSystem,
152 TransactionObj => $TransAsSystem);
155 #If it's applicable, prepare and commit it
157 $RT::Logger->debug ("$self: Checking condition ".$Scrip->ConditionObj->Name. "...\n");
159 if ( $Scrip->IsApplicable() ) {
161 $RT::Logger->debug ("$self: Matches condition ".$Scrip->ConditionObj->Name. "...\n");
162 #TODO: handle some errors here
164 $Scrip->ActionObj->LoadAction(TicketObj => $TicketAsSystem,
165 TransactionObj => $TransAsSystem);
168 if ($Scrip->Prepare()) {
169 $RT::Logger->debug("$self: Prepared " .
170 $Scrip->ActionObj->Name . "\n");
171 if ($Scrip->Commit()) {
172 $RT::Logger->debug("$self: Committed " .
173 $Scrip->ActionObj->Name . "\n");
176 $RT::Logger->info("$self: Failed to commit ".
177 $Scrip->ActionObj->Name . "\n");
181 $RT::Logger->info("$self: Failed to prepare " .
182 $Scrip->ActionObj->Name . "\n");
185 #We're done with it. lets clean up.
186 #TODO: something else isn't letting these get garbage collected. check em out.
187 $Scrip->ActionObj->DESTROY();
188 $Scrip->ConditionObj->DESTROY;
193 $RT::Logger->debug ("$self: Doesn't match condition ".$Scrip->ConditionObj->Name. "...\n");
195 # TODO: why doesn't this catch all the ScripObjs we create.
196 # and why do we explictly need to destroy them?
197 $Scrip->ConditionObj->DESTROY;
206 return ($id, "Transaction Created");
215 return (0, 'Deleting this object could break referential integrity');
220 # {{{ Routines dealing with Attachments
226 Returns the RT::Attachments Object which contains the "top-level" object
227 attachment for this transaction
235 if (!defined ($self->{'message'}) ){
237 $self->{'message'} = new RT::Attachments($self->CurrentUser);
238 $self->{'message'}->Limit(FIELD => 'TransactionId',
241 $self->{'message'}->ChildrenOf(0);
243 return($self->{'message'});
249 =head2 Content PARAMHASH
251 If this transaction has attached mime objects, returns the first text/ part.
252 Otherwise, returns undef.
254 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
255 at $args{'Wrap'}. $args{'Wrap'} defaults to 70.
262 my %args = ( Quote => 0,
268 # If we don\'t have any content, return undef now.
269 unless ($self->Message->First) {
273 # Get the set of toplevel attachments to this transaction.
274 my $MIMEObj = $self->Message->First();
276 # If it's a message or a plain part, just return the
278 if ($MIMEObj->ContentType() =~ '^(text|message)(/|$)') {
279 $content = $MIMEObj->Content();
282 # If it's a multipart object, first try returning the first
285 elsif ($MIMEObj->ContentType() =~ '^multipart/') {
286 my $plain_parts = $MIMEObj->Children();
287 $plain_parts->ContentType(VALUE => 'text/plain');
289 # If we actully found a part, return its content
290 if ($plain_parts->First &&
291 $plain_parts->First->Content ne '') {
292 $content = $plain_parts->First->Content;
295 # If that fails, return the first text/ or message/ part
296 # which has some content.
299 my $all_parts = $MIMEObj->Children();
300 while (($content == undef) &&
301 (my $part = $all_parts->Next)) {
302 if (($part->ContentType() =~ '^(text|message)(/|$)') and
303 ($part->Content())) {
304 $content = $part->Content;
310 # If all else fails, return a message that we couldn't find
313 $content = 'This transaction appears to have no content';
316 if ($args{'Quote'}) {
317 # Remove quoted signature.
318 $content =~ s/\n-- \n(.*)$//s;
320 # What's the longest line like?
321 foreach (split (/\n/,$content)) {
322 $max=length if ( length > $max);
326 require Text::Wrapper;
327 my $wrapper=new Text::Wrapper
329 columns => $args{'Wrap'},
330 body_start => ($max > 70*3 ? ' ' : ''),
333 $content=$wrapper->wrap($content);
336 $content =~ s/^/> /gm;
337 $content = '[' . $self->CreatorObj->Name() . ' - ' . $self->CreatedAsString()
351 If this transaction has attached mime objects, returns the first one's subject
352 Otherwise, returns null
358 if ($self->Message->First) {
359 return ($self->Message->First->Subject);
367 # {{{ sub Attachments
371 Returns all the RT::Attachment objects which are attached
372 to this transaction. Takes an optional parameter, which is
373 a ContentType that Attachments should be restricted to.
381 $Types = shift if (@_);
383 my $Attachments = new RT::Attachments($self->CurrentUser);
385 #If it's a comment, return an empty object if they don't have the right to see it
386 if ($self->Type eq 'Comment') {
387 unless ($self->CurrentUserHasRight('ShowTicketComments')) {
388 return ($Attachments);
391 #if they ain't got rights to see, return an empty object
393 unless ($self->CurrentUserHasRight('ShowTicket')) {
394 return ($Attachments);
398 $Attachments->Limit(FIELD => 'TransactionId',
401 # Get the attachments in the order they're put into
402 # the database. Arguably, we should be returning a tree
403 # of attachments, not a set...but no current app seems to need
406 $Attachments->OrderBy(ALIAS => 'main',
411 $Attachments->ContentType( VALUE => "$Types",
416 return($Attachments);
426 A private method used to attach a mime object to this transaction.
432 my $MIMEObject = shift;
434 if (!defined($MIMEObject)) {
435 $RT::Logger->error("$self _Attach: We can't attach a mime object if you don't give us one.\n");
436 return(0, "$self: no attachment specified");
441 my $Attachment = new RT::Attachment ($self->CurrentUser);
442 $Attachment->Create(TransactionId => $self->Id,
443 Attachment => $MIMEObject);
444 return ($Attachment, "Attachment created");
452 # {{{ Routines dealing with Transaction Attributes
458 Returns this transaction's ticket object.
464 if (! exists $self->{'TicketObj'}) {
465 $self->{'TicketObj'} = new RT::Ticket($self->CurrentUser);
466 $self->{'TicketObj'}->Load($self->Ticket);
469 return $self->{'TicketObj'};
473 # {{{ sub Description
477 Returns a text string which describes this transaction
486 #If it's a comment, we need to be extra special careful
487 if ($self->__Value('Type') eq 'Comment') {
488 unless ($self->CurrentUserHasRight('ShowTicketComments')) {
489 return (0, "Permission Denied");
493 #if they ain't got rights to see, don't let em
495 unless ($self->CurrentUserHasRight('ShowTicket')) {
496 return (0, "Permission Denied");
500 if (!defined($self->Type)) {
501 return("No transaction type specified");
504 return ($self->BriefDescription . " by " . $self->CreatorObj->Name);
509 # {{{ sub BriefDescription
511 =head2 BriefDescription
513 Returns a text string which briefly describes this transaction
518 sub BriefDescription {
522 #If it's a comment, we need to be extra special careful
523 if ($self->__Value('Type') eq 'Comment') {
524 unless ($self->CurrentUserHasRight('ShowTicketComments')) {
525 return (0, "Permission Denied");
529 #if they ain't got rights to see, don't let em
531 unless ($self->CurrentUserHasRight('ShowTicket')) {
532 return (0, "Permission Denied");
536 if (!defined($self->Type)) {
537 return("No transaction type specified");
540 if ($self->Type eq 'Create'){
541 return("Ticket created");
543 elsif ($self->Type =~ /Status/) {
544 if ($self->Field eq 'Status') {
545 if ($self->NewValue eq 'dead') {
546 return ("Ticket killed");
549 return( "Status changed from ". $self->OldValue .
550 " to ". $self->NewValue);
555 return ($self->Field." changed from ".($self->OldValue||"(empty value)").
556 " to ".$self->NewValue );
559 if ($self->Type eq 'Correspond') {
560 return("Correspondence added");
563 elsif ($self->Type eq 'Comment') {
564 return( "Comments added");
567 elsif ($self->Type eq 'Keyword') {
569 my $field = 'Keyword';
572 my $keywordsel = new RT::KeywordSelect ($self->CurrentUser);
573 $keywordsel->Load($self->Field);
574 $field = $keywordsel->Name();
577 if ($self->OldValue eq '') {
578 return ($field." ".$self->NewValue." added");
580 elsif ($self->NewValue eq '') {
581 return ($field." ".$self->OldValue." deleted");
585 return ($field." ".$self->OldValue . " changed to ".
590 elsif ($self->Type eq 'Untake'){
594 elsif ($self->Type eq "Take") {
598 elsif ($self->Type eq "Force") {
599 my $Old = RT::User->new($self->CurrentUser);
600 $Old->Load($self->OldValue);
601 my $New = RT::User->new($self->CurrentUser);
602 $New->Load($self->NewValue);
603 return "Owner forcibly changed from ".$Old->Name . " to ". $New->Name;
605 elsif ($self->Type eq "Steal") {
606 my $Old = RT::User->new($self->CurrentUser);
607 $Old->Load($self->OldValue);
608 return "Stolen from ".$Old->Name;
611 elsif ($self->Type eq "Give") {
612 my $New = RT::User->new($self->CurrentUser);
613 $New->Load($self->NewValue);
614 return( "Given to ".$New->Name);
617 elsif ($self->Type eq 'AddWatcher'){
618 return( $self->Field." ". $self->NewValue ." added");
621 elsif ($self->Type eq 'DelWatcher'){
622 return( $self->Field." ".$self->OldValue ." deleted");
625 elsif ($self->Type eq 'Subject') {
626 return( "Subject changed to ".$self->Data);
628 elsif ($self->Type eq 'Told') {
629 return( "User notified");
632 elsif ($self->Type eq 'AddLink') {
633 return ($self->Data);
635 elsif ($self->Type eq 'DeleteLink') {
636 return ($self->Data);
638 elsif ($self->Type eq 'Set') {
639 if ($self->Field eq 'Queue') {
640 my $q1 = new RT::Queue($self->CurrentUser);
641 $q1->Load($self->OldValue);
642 my $q2 = new RT::Queue($self->CurrentUser);
643 $q2->Load($self->NewValue);
644 return ($self->Field . " changed from " . $q1->Name . " to ".
648 # Write the date/time change at local time:
649 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
650 my $t1 = new RT::Date($self->CurrentUser);
651 $t1->Set(Format => 'ISO', Value => $self->NewValue);
652 my $t2 = new RT::Date($self->CurrentUser);
653 $t2->Set(Format => 'ISO', Value => $self->OldValue);
654 return ($self->Field . " changed from " . $t2->AsString .
655 " to ".$t1->AsString);
658 return ($self->Field . " changed from " . $self->OldValue .
659 " to ".$self->NewValue);
662 elsif ($self->Type eq 'PurgeTransaction') {
663 return ("Transaction ".$self->Data. " purged");
666 return ("Default: ". $self->Type ."/". $self->Field .
667 " changed from " . $self->OldValue .
668 " to ".$self->NewValue);
675 # {{{ Utility methods
681 Returns true if the creator of the transaction is a requestor of the ticket.
682 Returns false otherwise
688 return ($self->TicketObj->IsRequestor($self->CreatorObj));
695 # {{{ sub _Accessible
701 Ticket => 'read/public',
707 Creator => 'read/auto',
708 Created => 'read/auto',
710 return $self->SUPER::_Accessible(@_, %Cols);
721 return(0, '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
741 #if the field is public, return it.
742 if ($self->_Accessible($field, 'public')) {
743 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')) {
752 #if they ain't got rights to see, don't let em
754 unless ($self->CurrentUserHasRight('ShowTicket')) {
759 return($self->__Value($field));
765 # {{{ sub CurrentUserHasRight
767 =head2 CurrentUserHasRight RIGHT
769 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
774 sub CurrentUserHasRight {
777 return ($self->CurrentUser->HasQueueRight(Right => "$right",
778 TicketObj => $self->TicketObj));