rt 3.6.10
[freeside.git] / rt / lib / RT / Transaction_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC 
6 #                                          <jesse@bestpractical.com>
7
8 # (Except where explicitly superseded by other copyright notices)
9
10
11 # LICENSE:
12
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
16 # from www.gnu.org.
17
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.
22
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.
28
29
30 # CONTRIBUTION SUBMISSION POLICY:
31
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.)
37
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.
46
47 # END BPS TAGGED BLOCK }}}
48 =head1 NAME
49
50   RT::Transaction - RT\'s transaction object
51
52 =head1 SYNOPSIS
53
54   use RT::Transaction;
55
56
57 =head1 DESCRIPTION
58
59
60 Each RT::Transaction describes an atomic change to a ticket object 
61 or an update to an RT::Ticket object.
62 It can have arbitrary MIME attachments.
63
64
65 =head1 METHODS
66
67 =begin testing
68
69 ok(require RT::Transaction);
70
71 =end testing
72
73 =cut
74
75
76 package RT::Transaction;
77
78 use strict;
79 no warnings qw(redefine);
80
81 use vars qw( %_BriefDescriptions $PreferredContentType );
82
83 use RT::Attachments;
84 use RT::Scrips;
85
86 use HTML::FormatText;
87 use HTML::TreeBuilder;
88
89
90 # {{{ sub Create 
91
92 =head2 Create
93
94 Create a new transaction.
95
96 This routine should _never_ be called by anything other than RT::Ticket. 
97 It should not be called 
98 from client code. Ever. Not ever.  If you do this, we will hunt you down and break your kneecaps.
99 Then the unpleasant stuff will start.
100
101 TODO: Document what gets passed to this
102
103 =cut
104
105 sub Create {
106     my $self = shift;
107     my %args = (
108         id             => undef,
109         TimeTaken      => 0,
110         Type           => 'undefined',
111         Data           => '',
112         Field          => undef,
113         OldValue       => undef,
114         NewValue       => undef,
115         MIMEObj        => undef,
116         ActivateScrips => 1,
117         CommitScrips => 1,
118         ObjectType => 'RT::Ticket',
119         ObjectId => 0,
120         ReferenceType => undef,
121         OldReference       => undef,
122         NewReference       => undef,
123         @_
124     );
125
126     $args{ObjectId} ||= $args{Ticket};
127
128     #if we didn't specify a ticket, we need to bail
129     unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
130         return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
131     }
132
133
134
135     #lets create our transaction
136     my %params = (
137         Type      => $args{'Type'},
138         Data      => $args{'Data'},
139         Field     => $args{'Field'},
140         OldValue  => $args{'OldValue'},
141         NewValue  => $args{'NewValue'},
142         Created   => $args{'Created'},
143         ObjectType => $args{'ObjectType'},
144         ObjectId => $args{'ObjectId'},
145         ReferenceType => $args{'ReferenceType'},
146         OldReference => $args{'OldReference'},
147         NewReference => $args{'NewReference'},
148     );
149
150     # Parameters passed in during an import that we probably don't want to touch, otherwise
151     foreach my $attr qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy) {
152         $params{$attr} = $args{$attr} if ($args{$attr});
153     }
154  
155     my $id = $self->SUPER::Create(%params);
156     $self->Load($id);
157     if ( defined $args{'MIMEObj'} ) {
158         my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
159         unless ( $id ) {
160             $RT::Logger->error("Couldn't add attachment: $msg");
161             return ( 0, $self->loc("Couldn't add attachment") );
162         }
163     }
164
165
166     #Provide a way to turn off scrips if we need to
167         $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
168     if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
169        $self->{'scrips'} = RT::Scrips->new($RT::SystemUser);
170
171         $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id); 
172
173         $self->{'scrips'}->Prepare(
174             Stage       => 'TransactionCreate',
175             Type        => $args{'Type'},
176             Ticket      => $args{'ObjectId'},
177             Transaction => $self->id,
178         );
179         if ($args{'CommitScrips'} ) {
180             $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
181             $self->{'scrips'}->Commit();
182         }
183     }
184
185     return ( $id, $self->loc("Transaction Created") );
186 }
187
188 # }}}
189
190 =head2 Scrips
191
192 Returns the Scrips object for this transaction.
193 This routine is only useful on a freshly created transaction object.
194 Scrips do not get persisted to the database with transactions.
195
196
197 =cut
198
199
200 sub Scrips {
201     my $self = shift;
202     return($self->{'scrips'});
203 }
204
205
206 # {{{ sub Delete
207
208 =head2 Delete
209
210 Delete this transaction. Currently DOES NOT CHECK ACLS
211
212 =cut
213
214 sub Delete {
215     my $self = shift;
216
217
218     $RT::Handle->BeginTransaction();
219
220     my $attachments = $self->Attachments;
221
222     while (my $attachment = $attachments->Next) {
223         my ($id, $msg) = $attachment->Delete();
224         unless ($id) {
225             $RT::Handle->Rollback();
226             return($id, $self->loc("System Error: [_1]", $msg));
227         }
228     }
229     my ($id,$msg) = $self->SUPER::Delete();
230         unless ($id) {
231             $RT::Handle->Rollback();
232             return($id, $self->loc("System Error: [_1]", $msg));
233         }
234     $RT::Handle->Commit();
235     return ($id,$msg);
236 }
237
238 # }}}
239
240 # {{{ Routines dealing with Attachments
241
242 # {{{ sub Message 
243
244 =head2 Message
245
246   Returns the RT::Attachments Object which contains the "top-level"object
247   attachment for this transaction
248
249 =cut
250
251 sub Message {
252
253     my $self = shift;
254     
255     if ( !defined( $self->{'message'} ) ) {
256
257         $self->{'message'} = new RT::Attachments( $self->CurrentUser );
258         $self->{'message'}->Limit(
259             FIELD => 'TransactionId',
260             VALUE => $self->Id
261         );
262
263         $self->{'message'}->ChildrenOf(0);
264     }
265     return ( $self->{'message'} );
266 }
267
268 # }}}
269
270 # {{{ sub Content
271
272 =head2 Content PARAMHASH
273
274 If this transaction has attached mime objects, returns the body of the first
275 textual part (as defined in RT::I18N::IsTextualContentType).  Otherwise,
276 returns undef.
277
278 Takes a paramhash.  If the $args{'Quote'} parameter is set, wraps this message 
279 at $args{'Wrap'}.  $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
280
281 If $args{'Type'} is set to C<text/html>, plain texts are upgraded to HTML.
282 Otherwise, HTML texts are downgraded to plain text.  If $args{'Type'} is
283 missing, it defaults to the value of C<$RT::Transaction::PreferredContentType>.
284
285 =cut
286
287 sub Content {
288     my $self = shift;
289     my %args = (
290         Type  => $PreferredContentType,
291         Quote => 0,
292         Wrap  => 70,
293         Wrap  => ( $RT::MessageBoxWidth || 72 ) - 2,
294         @_
295     );
296
297     my $content;
298     if (my $content_obj = $self->ContentObj) {
299         $content = $content_obj->Content;
300
301         if ($content_obj->ContentType =~ m{^text/html$}i) {
302             $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
303
304             if ($args{Type} ne 'text/html') {
305                 $content = HTML::FormatText->new(
306                     leftmargin  => 0,
307                     rightmargin => 78,
308                 )->format(
309                     HTML::TreeBuilder->new_from_content( $content )
310                 );
311             }
312         }
313         else {
314             $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
315
316             if ($args{Type} eq 'text/html') {
317                 # Extremely simple text->html converter
318                 $content =~ s/&/&#38;/g;
319                 $content =~ s/</&lt;/g;
320                 $content =~ s/>/&gt;/g;
321                 $content = "<pre>$content</pre>";
322             }
323         }
324     }
325
326     # If all else fails, return a message that we couldn't find any content
327     else {
328         $content = $self->loc('This transaction appears to have no content');
329     }
330
331     if ( $args{'Quote'} ) {
332
333         # What's the longest line like?
334         my $max = 0;
335         foreach ( split ( /\n/, $content ) ) {
336             $max = length if ( length > $max );
337         }
338
339         if ( $max > $args{'Wrap'}+6 ) { # 76 ) {
340             require Text::Wrapper;
341             my $wrapper = new Text::Wrapper(
342                 columns    => $args{'Wrap'},
343                 body_start => ( $max > 70 * 3 ? '   ' : '' ),
344                 par_start  => ''
345             );
346             $content = $wrapper->wrap($content);
347         }
348
349         $content =~ s/^/> /gm;
350         $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString(), $self->CreatorObj->Name())
351           . "\n$content\n\n";
352     }
353
354     return ($content);
355 }
356
357 # }}}
358
359 # {{{ ContentObj
360
361 =head2 ContentObj 
362
363 Returns the RT::Attachment object which contains the content for this Transaction
364
365 =cut
366
367
368 sub ContentObj {
369
370     my $self = shift;
371
372     # If we don\'t have any content, return undef now.
373     unless ( $self->Attachments->First ) {
374         return (undef);
375     }
376
377     # Get the set of toplevel attachments to this transaction.
378     my $Attachment = $self->Attachments->First();
379
380     # If it's a textual part, just return the body.
381     if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
382         return ($Attachment);
383     }
384
385     # If it's a multipart object, first try returning the first part with preferred
386     # MIME type ('text/plain' by default).
387
388     elsif ( $Attachment->ContentType() =~ '^multipart/' ) {
389         my $plain_parts = $Attachment->Children();
390         $plain_parts->ContentType( VALUE => ($PreferredContentType || 'text/plain') );
391
392         # If we actully found a part, return its content
393         if ( $plain_parts->First && $plain_parts->First->Content ne '' ) {
394             return ( $plain_parts->First );
395         }
396
397
398         # If that fails, return the first textual part which has some content.
399
400         else {
401             my $all_parts = $self->Attachments();
402             while ( my $part = $all_parts->Next ) {
403                 if ( ( RT::I18N::IsTextualContentType($part->ContentType) ) and ( $part->Content() ne '' ) ) {
404                     return ($part);
405                 }
406             }
407         }
408
409     }
410
411     # We found no content. suck
412     return (undef);
413 }
414
415 # }}}
416
417 # {{{ sub Subject
418
419 =head2 Subject
420
421 If this transaction has attached mime objects, returns the first one's subject
422 Otherwise, returns null
423   
424 =cut
425
426 sub Subject {
427     my $self = shift;
428     if ( $self->Attachments->First ) {
429         return ( $self->Attachments->First->Subject );
430     }
431     else {
432         return (undef);
433     }
434 }
435
436 # }}}
437
438 # {{{ sub Attachments 
439
440 =head2 Attachments
441
442   Returns all the RT::Attachment objects which are attached
443 to this transaction. Takes an optional parameter, which is
444 a ContentType that Attachments should be restricted to.
445
446 =cut
447
448 sub Attachments {
449     my $self = shift;
450
451     unless ( $self->{'attachments'} ) {
452         $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
453
454         #If it's a comment, return an empty object if they don't have the right to see it
455         if ( $self->Type eq 'Comment' ) {
456             unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
457                 return ( $self->{'attachments'} );
458             }
459         }
460
461         #if they ain't got rights to see, return an empty object
462         elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
463             unless ( $self->CurrentUserHasRight('ShowTicket') ) {
464                 return ( $self->{'attachments'} );
465             }
466         }
467
468         $self->{'attachments'}->Limit( FIELD => 'TransactionId',
469                                        VALUE => $self->Id );
470
471         # Get the self->{'attachments'} in the order they're put into
472         # the database.  Arguably, we should be returning a tree
473         # of self->{'attachments'}, not a set...but no current app seems to need
474         # it.
475
476         $self->{'attachments'}->OrderBy( ALIAS => 'main',
477                                          FIELD => 'id',
478                                          ORDER => 'asc' );
479
480     }
481     return ( $self->{'attachments'} );
482
483 }
484
485 # }}}
486
487 # {{{ sub _Attach 
488
489 =head2 _Attach
490
491 A private method used to attach a mime object to this transaction.
492
493 =cut
494
495 sub _Attach {
496     my $self       = shift;
497     my $MIMEObject = shift;
498
499     if ( !defined($MIMEObject) ) {
500         $RT::Logger->error(
501 "$self _Attach: We can't attach a mime object if you don't give us one.\n"
502         );
503         return ( 0, $self->loc("[_1]: no attachment specified", $self) );
504     }
505
506     my $Attachment = new RT::Attachment( $self->CurrentUser );
507     my ($id, $msg) = $Attachment->Create(
508         TransactionId => $self->Id,
509         Attachment    => $MIMEObject
510     );
511     return ( $Attachment, $msg || $self->loc("Attachment created") );
512
513 }
514
515 # }}}
516
517 # }}}
518
519 # {{{ Routines dealing with Transaction Attributes
520
521 # {{{ sub Description 
522
523 =head2 Description
524
525 Returns a text string which describes this transaction
526
527 =cut
528
529 sub Description {
530     my $self = shift;
531
532     #Check those ACLs
533     #If it's a comment or a comment email record,
534     #  we need to be extra special careful
535
536     if ( $self->__Value('Type') =~ /^Comment/ ) {
537         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
538             return ( $self->loc("Permission Denied") );
539         }
540     }
541
542     #if they ain't got rights to see, don't let em
543     elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
544         unless ( $self->CurrentUserHasRight('ShowTicket') ) {
545             return ($self->loc("Permission Denied") );
546         }
547     }
548
549     if ( !defined( $self->Type ) ) {
550         return ( $self->loc("No transaction type specified"));
551     }
552
553     return ( $self->loc("[_1] by [_2]",$self->BriefDescription , $self->CreatorObj->Name ));
554 }
555
556 # }}}
557
558 # {{{ sub BriefDescription 
559
560 =head2 BriefDescription
561
562 Returns a text string which briefly describes this transaction
563
564 =cut
565
566 sub BriefDescription {
567     my $self = shift;
568
569     #If it's a comment or a comment email record,
570     #  we need to be extra special careful
571     if ( $self->__Value('Type') =~ /^Comment/ ) {
572         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
573             return ( $self->loc("Permission Denied") );
574         }
575     }
576
577     #if they ain't got rights to see, don't let em
578     elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
579         unless ( $self->CurrentUserHasRight('ShowTicket') ) {
580             return ( $self->loc("Permission Denied") );
581         }
582     }
583
584     my $type = $self->Type;    #cache this, rather than calling it 30 times
585
586     if ( !defined($type) ) {
587         return $self->loc("No transaction type specified");
588     }
589
590     my $obj_type = $self->FriendlyObjectType;
591
592     if ( $type eq 'Create' ) {
593         return ( $self->loc( "[_1] created", $obj_type ) );
594     }
595     elsif ( $type =~ /Status/ ) {
596         if ( $self->Field eq 'Status' ) {
597             if ( $self->NewValue eq 'deleted' ) {
598                 return ( $self->loc( "[_1] deleted", $obj_type ) );
599             }
600             else {
601                 return (
602                     $self->loc(
603                         "Status changed from [_1] to [_2]",
604                         "'" . $self->loc( $self->OldValue ) . "'",
605                         "'" . $self->loc( $self->NewValue ) . "'"
606                     )
607                 );
608
609             }
610         }
611
612         # Generic:
613         my $no_value = $self->loc("(no value)");
614         return (
615             $self->loc(
616                 "[_1] changed from [_2] to [_3]",
617                 $self->Field,
618                 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
619                 "'" . $self->NewValue . "'"
620             )
621         );
622     }
623
624     if ( my $code = $_BriefDescriptions{$type} ) {
625         return $code->($self);
626     }
627
628     return $self->loc(
629         "Default: [_1]/[_2] changed from [_3] to [_4]",
630         $type,
631         $self->Field,
632         (
633             $self->OldValue
634             ? "'" . $self->OldValue . "'"
635             : $self->loc("(no value)")
636         ),
637         "'" . $self->NewValue . "'"
638     );
639 }
640
641 %_BriefDescriptions = (
642     CommentEmailRecord => sub {
643         my $self = shift;
644         return $self->loc("Outgoing email about a comment recorded");
645     },
646     EmailRecord => sub {
647         my $self = shift;
648         return $self->loc("Outgoing email recorded");
649     },
650     Correspond => sub {
651         my $self = shift;
652         return $self->loc("Correspondence added");
653     },
654     Comment => sub {
655         my $self = shift;
656         return $self->loc("Comments added");
657     },
658     CustomField => sub {
659         my $self = shift;
660         my $field = $self->loc('CustomField');
661
662         if ( $self->Field ) {
663             my $cf = RT::CustomField->new( $self->CurrentUser );
664             $cf->Load( $self->Field );
665             $field = $cf->Name();
666         }
667
668         if ( $self->OldValue eq '' ) {
669             return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
670         }
671         elsif ( $self->NewValue eq '' ) {
672             return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
673
674         }
675         else {
676             return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
677         }
678     },
679     Untake => sub {
680         my $self = shift;
681         return $self->loc("Untaken");
682     },
683     Take => sub {
684         my $self = shift;
685         return $self->loc("Taken");
686     },
687     Force => sub {
688         my $self = shift;
689         my $Old = RT::User->new( $self->CurrentUser );
690         $Old->Load( $self->OldValue );
691         my $New = RT::User->new( $self->CurrentUser );
692         $New->Load( $self->NewValue );
693
694         return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
695     },
696     Steal => sub {
697         my $self = shift;
698         my $Old = RT::User->new( $self->CurrentUser );
699         $Old->Load( $self->OldValue );
700         return $self->loc("Stolen from [_1]",  $Old->Name);
701     },
702     Give => sub {
703         my $self = shift;
704         my $New = RT::User->new( $self->CurrentUser );
705         $New->Load( $self->NewValue );
706         return $self->loc( "Given to [_1]",  $New->Name );
707     },
708     AddWatcher => sub {
709         my $self = shift;
710         my $principal = RT::Principal->new($self->CurrentUser);
711         $principal->Load($self->NewValue);
712         return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
713     },
714     DelWatcher => sub {
715         my $self = shift;
716         my $principal = RT::Principal->new($self->CurrentUser);
717         $principal->Load($self->OldValue);
718         return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
719     },
720     Subject => sub {
721         my $self = shift;
722         return $self->loc( "Subject changed to [_1]", $self->Data );
723     },
724     AddLink => sub {
725         my $self = shift;
726         my $value;
727         if ( $self->NewValue ) {
728             my $URI = RT::URI->new( $self->CurrentUser );
729             $URI->FromURI( $self->NewValue );
730             if ( $URI->Resolver ) {
731                 $value = $URI->Resolver->AsString;
732             }
733             else {
734                 $value = $self->NewValue;
735             }
736             if ( $self->Field eq 'DependsOn' ) {
737                 return $self->loc( "Dependency on [_1] added", $value );
738             }
739             elsif ( $self->Field eq 'DependedOnBy' ) {
740                 return $self->loc( "Dependency by [_1] added", $value );
741
742             }
743             elsif ( $self->Field eq 'RefersTo' ) {
744                 return $self->loc( "Reference to [_1] added", $value );
745             }
746             elsif ( $self->Field eq 'ReferredToBy' ) {
747                 return $self->loc( "Reference by [_1] added", $value );
748             }
749             elsif ( $self->Field eq 'MemberOf' ) {
750                 return $self->loc( "Membership in [_1] added", $value );
751             }
752             elsif ( $self->Field eq 'HasMember' ) {
753                 return $self->loc( "Member [_1] added", $value );
754             }
755             elsif ( $self->Field eq 'MergedInto' ) {
756                 return $self->loc( "Merged into [_1]", $value );
757             }
758         }
759         else {
760             return ( $self->Data );
761         }
762     },
763     DeleteLink => sub {
764         my $self = shift;
765         my $value;
766         if ( $self->OldValue ) {
767             my $URI = RT::URI->new( $self->CurrentUser );
768             $URI->FromURI( $self->OldValue );
769             if ( $URI->Resolver ) {
770                 $value = $URI->Resolver->AsString;
771             }
772             else {
773                 $value = $self->OldValue;
774             }
775
776             if ( $self->Field eq 'DependsOn' ) {
777                 return $self->loc( "Dependency on [_1] deleted", $value );
778             }
779             elsif ( $self->Field eq 'DependedOnBy' ) {
780                 return $self->loc( "Dependency by [_1] deleted", $value );
781
782             }
783             elsif ( $self->Field eq 'RefersTo' ) {
784                 return $self->loc( "Reference to [_1] deleted", $value );
785             }
786             elsif ( $self->Field eq 'ReferredToBy' ) {
787                 return $self->loc( "Reference by [_1] deleted", $value );
788             }
789             elsif ( $self->Field eq 'MemberOf' ) {
790                 return $self->loc( "Membership in [_1] deleted", $value );
791             }
792             elsif ( $self->Field eq 'HasMember' ) {
793                 return $self->loc( "Member [_1] deleted", $value );
794             }
795         }
796         else {
797             return ( $self->Data );
798         }
799     },
800     Set => sub {
801         my $self = shift;
802         if ( $self->Field eq 'Password' ) {
803             return $self->loc('Password changed');
804         }
805         elsif ( $self->Field eq 'Queue' ) {
806             my $q1 = new RT::Queue( $self->CurrentUser );
807             $q1->Load( $self->OldValue );
808             my $q2 = new RT::Queue( $self->CurrentUser );
809             $q2->Load( $self->NewValue );
810             return $self->loc("[_1] changed from [_2] to [_3]", $self->Field , $q1->Name , $q2->Name);
811         }
812
813         # Write the date/time change at local time:
814         elsif ($self->Field =~  /Due|Starts|Started|Told/) {
815             my $t1 = new RT::Date($self->CurrentUser);
816             $t1->Set(Format => 'ISO', Value => $self->NewValue);
817             my $t2 = new RT::Date($self->CurrentUser);
818             $t2->Set(Format => 'ISO', Value => $self->OldValue);
819             return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, $t2->AsString, $t1->AsString );
820         }
821         else {
822             return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
823         }
824     },
825     PurgeTransaction => sub {
826         my $self = shift;
827         return $self->loc("Transaction [_1] purged", $self->Data);
828     },
829     AddReminder => sub {
830         my $self = shift;
831         my $ticket = RT::Ticket->new($self->CurrentUser);
832         $ticket->Load($self->NewValue);
833         return $self->loc("Reminder '[_1]' added", $ticket->Subject);
834     },
835     OpenReminder => sub {
836         my $self = shift;
837         my $ticket = RT::Ticket->new($self->CurrentUser);
838         $ticket->Load($self->NewValue);
839         return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
840     
841     },
842     ResolveReminder => sub {
843         my $self = shift;
844         my $ticket = RT::Ticket->new($self->CurrentUser);
845         $ticket->Load($self->NewValue);
846         return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
847     
848     
849     }
850 );
851
852 # }}}
853
854 # {{{ Utility methods
855
856 # {{{ sub IsInbound
857
858 =head2 IsInbound
859
860 Returns true if the creator of the transaction is a requestor of the ticket.
861 Returns false otherwise
862
863 =cut
864
865 sub IsInbound {
866     my $self = shift;
867     $self->ObjectType eq 'RT::Ticket' or return undef;
868     return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
869 }
870
871 # }}}
872
873 # }}}
874
875 sub _OverlayAccessible {
876     {
877
878           ObjectType => { public => 1},
879           ObjectId => { public => 1},
880
881     }
882 };
883
884 # }}}
885
886 # }}}
887
888 # {{{ sub _Set
889
890 sub _Set {
891     my $self = shift;
892     return ( 0, $self->loc('Transactions are immutable') );
893 }
894
895 # }}}
896
897 # {{{ sub _Value 
898
899 =head2 _Value
900
901 Takes the name of a table column.
902 Returns its value as a string, if the user passes an ACL check
903
904 =cut
905
906 sub _Value {
907
908     my $self  = shift;
909     my $field = shift;
910
911     #if the field is public, return it.
912     if ( $self->_Accessible( $field, 'public' ) ) {
913         return ( $self->__Value($field) );
914
915     }
916
917     #If it's a comment, we need to be extra special careful
918     if ( $self->__Value('Type') eq 'Comment' ) {
919         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
920             return (undef);
921         }
922     }
923     elsif ( $self->__Value('Type') eq 'CommentEmailRecord' ) {
924         unless ( $self->CurrentUserHasRight('ShowTicketComments')
925             && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
926             return (undef);
927         }
928
929     }
930     elsif ( $self->__Value('Type') eq 'EmailRecord' ) {
931         unless ( $self->CurrentUserHasRight('ShowOutgoingEmail')) {
932             return (undef);
933         }
934
935     }
936     # Make sure the user can see the custom field before showing that it changed
937     elsif ( ( $self->__Value('Type') eq 'CustomField' ) && $self->__Value('Field') ) {
938         my $cf = RT::CustomField->new( $self->CurrentUser );
939         $cf->Load( $self->__Value('Field') );
940         return (undef) unless ( $cf->CurrentUserHasRight('SeeCustomField') );
941     }
942
943
944     #if they ain't got rights to see, don't let em
945     elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
946         unless ( $self->CurrentUserHasRight('ShowTicket') ) {
947             return (undef);
948         }
949     }
950
951     return ( $self->__Value($field) );
952
953 }
954
955 # }}}
956
957 # {{{ sub CurrentUserHasRight
958
959 =head2 CurrentUserHasRight RIGHT
960
961 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
962 passed in here.
963
964 =cut
965
966 sub CurrentUserHasRight {
967     my $self  = shift;
968     my $right = shift;
969     return (
970         $self->CurrentUser->HasRight(
971             Right     => "$right",
972             Object => $self->TicketObj
973           )
974     );
975 }
976
977 # }}}
978
979 sub Ticket {
980     my $self = shift;
981     return $self->ObjectId;
982 }
983
984 sub TicketObj {
985     my $self = shift;
986     return $self->Object;
987 }
988
989 sub OldValue {
990     my $self = shift;
991     if ( my $type = $self->__Value('ReferenceType')
992          and my $id = $self->__Value('OldReference') )
993     {
994         my $Object = $type->new($self->CurrentUser);
995         $Object->Load( $id );
996         return $Object->Content;
997     }
998     else {
999         return $self->__Value('OldValue');
1000     }
1001 }
1002
1003 sub NewValue {
1004     my $self = shift;
1005     if ( my $type = $self->__Value('ReferenceType')
1006          and my $id = $self->__Value('NewReference') )
1007     {
1008         my $Object = $type->new($self->CurrentUser);
1009         $Object->Load( $id );
1010         return $Object->Content;
1011     }
1012     else {
1013         return $self->__Value('NewValue');
1014     }
1015 }
1016
1017 sub Object {
1018     my $self  = shift;
1019     my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1020     $Object->Load($self->__Value('ObjectId'));
1021     return($Object);
1022 }
1023
1024 sub FriendlyObjectType {
1025     my $self = shift;
1026     my $type = $self->ObjectType or return undef;
1027     $type =~ s/^RT:://;
1028     return $self->loc($type);
1029 }
1030
1031 =head2 UpdateCustomFields
1032     
1033     Takes a hash of 
1034
1035     CustomField-<<Id>> => Value
1036         or 
1037
1038     Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1039     this transaction's custom fields
1040
1041 =cut
1042
1043 sub UpdateCustomFields {
1044     my $self = shift;
1045     my %args = (@_);
1046
1047     # This method used to have an API that took a hash of a single
1048     # value "ARGSRef", which was a reference to a hash of arguments.
1049     # This was insane. The next few lines of code preserve that API
1050     # while giving us something saner.
1051        
1052
1053     # TODO: 3.6: DEPRECATE OLD API
1054
1055     my $args; 
1056
1057     if ($args{'ARGSRef'}) { 
1058         $args = $args{ARGSRef};
1059     } else {
1060         $args = \%args;
1061     }
1062
1063     foreach my $arg ( keys %$args ) {
1064         next
1065           unless ( $arg =~
1066             /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1067         next if $arg =~ /-Magic$/;
1068         my $cfid   = $1;
1069         my $values = $args->{$arg};
1070         foreach
1071           my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1072         {
1073             next unless length($value);
1074             $self->_AddCustomFieldValue(
1075                 Field             => $cfid,
1076                 Value             => $value,
1077                 RecordTransaction => 0,
1078             );
1079         }
1080     }
1081 }
1082
1083
1084
1085 =head2 CustomFieldValues
1086
1087  Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1088
1089  See L<RT::Record>
1090
1091 =cut
1092
1093 sub CustomFieldValues {
1094     my $self  = shift;
1095     my $field = shift;
1096
1097     if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1098
1099         unless ( defined $field && $field =~ /^\d+$/o ) {
1100             my $CFs = RT::CustomFields->new( $self->CurrentUser );
1101              $CFs->Limit( FIELD => 'Name', VALUE => $field);
1102             $CFs->LimitToLookupType($self->CustomFieldLookupType);
1103             $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1104             $field = $CFs->First->id if $CFs->First;
1105         }
1106     }
1107     return $self->SUPER::CustomFieldValues($field);
1108 }
1109
1110 # }}}
1111
1112 # {{{ sub CustomFieldLookupType
1113
1114 =head2 CustomFieldLookupType
1115
1116 Returns the RT::Transaction lookup type, which can 
1117 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1118
1119 =cut
1120
1121 # }}}
1122
1123 sub CustomFieldLookupType {
1124     "RT::Queue-RT::Ticket-RT::Transaction";
1125 }
1126
1127 # Transactions don't change. by adding this cache congif directiove, we don't lose pathalogically on long tickets.
1128 sub _CacheConfig {
1129   {
1130      'cache_p'        => 1,
1131      'fast_update_p'  => 1,
1132      'cache_for_sec'  => 6000,
1133   }
1134 }
1135 1;