import rt 3.6.6
[freeside.git] / rt / lib / RT / Transaction_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2007 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/copyleft/gpl.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 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         @_
294     );
295
296     my $content;
297     if (my $content_obj = $self->ContentObj) {
298         $content = $content_obj->Content;
299
300         if ($content_obj->ContentType =~ m{^text/html$}i) {
301             $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
302
303             if ($args{Type} ne 'text/html') {
304                 $content = HTML::FormatText->new(
305                     leftmargin  => 0,
306                     rightmargin => 78,
307                 )->format(
308                     HTML::TreeBuilder->new_from_content( $content )
309                 );
310             }
311         }
312         else {
313             $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
314
315             if ($args{Type} eq 'text/html') {
316                 # Extremely simple text->html converter
317                 $content =~ s/&/&#38;/g;
318                 $content =~ s/</&lt;/g;
319                 $content =~ s/>/&gt;/g;
320                 $content = "<pre>$content</pre>";
321             }
322         }
323     }
324
325     # If all else fails, return a message that we couldn't find any content
326     else {
327         $content = $self->loc('This transaction appears to have no content');
328     }
329
330     if ( $args{'Quote'} ) {
331
332         # What's the longest line like?
333         my $max = 0;
334         foreach ( split ( /\n/, $content ) ) {
335             $max = length if ( length > $max );
336         }
337
338         if ( $max > 76 ) {
339             require Text::Wrapper;
340             my $wrapper = new Text::Wrapper(
341                 columns    => $args{'Wrap'},
342                 body_start => ( $max > 70 * 3 ? '   ' : '' ),
343                 par_start  => ''
344             );
345             $content = $wrapper->wrap($content);
346         }
347
348         $content =~ s/^/> /gm;
349         $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString(), $self->CreatorObj->Name())
350           . "\n$content\n\n";
351     }
352
353     return ($content);
354 }
355
356 # }}}
357
358 # {{{ ContentObj
359
360 =head2 ContentObj 
361
362 Returns the RT::Attachment object which contains the content for this Transaction
363
364 =cut
365
366
367 sub ContentObj {
368
369     my $self = shift;
370
371     # If we don\'t have any content, return undef now.
372     unless ( $self->Attachments->First ) {
373         return (undef);
374     }
375
376     # Get the set of toplevel attachments to this transaction.
377     my $Attachment = $self->Attachments->First();
378
379     # If it's a textual part, just return the body.
380     if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
381         return ($Attachment);
382     }
383
384     # If it's a multipart object, first try returning the first part with preferred
385     # MIME type ('text/plain' by default).
386
387     elsif ( $Attachment->ContentType() =~ '^multipart/' ) {
388         my $plain_parts = $Attachment->Children();
389         $plain_parts->ContentType( VALUE => ($PreferredContentType || 'text/plain') );
390
391         # If we actully found a part, return its content
392         if ( $plain_parts->First && $plain_parts->First->Content ne '' ) {
393             return ( $plain_parts->First );
394         }
395
396
397         # If that fails, return the first textual part which has some content.
398
399         else {
400             my $all_parts = $self->Attachments();
401             while ( my $part = $all_parts->Next ) {
402                 if ( ( RT::I18N::IsTextualContentType($part->ContentType) ) and ( $part->Content() ne '' ) ) {
403                     return ($part);
404                 }
405             }
406         }
407
408     }
409
410     # We found no content. suck
411     return (undef);
412 }
413
414 # }}}
415
416 # {{{ sub Subject
417
418 =head2 Subject
419
420 If this transaction has attached mime objects, returns the first one's subject
421 Otherwise, returns null
422   
423 =cut
424
425 sub Subject {
426     my $self = shift;
427     if ( $self->Attachments->First ) {
428         return ( $self->Attachments->First->Subject );
429     }
430     else {
431         return (undef);
432     }
433 }
434
435 # }}}
436
437 # {{{ sub Attachments 
438
439 =head2 Attachments
440
441   Returns all the RT::Attachment objects which are attached
442 to this transaction. Takes an optional parameter, which is
443 a ContentType that Attachments should be restricted to.
444
445 =cut
446
447 sub Attachments {
448     my $self = shift;
449
450     unless ( $self->{'attachments'} ) {
451         $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
452
453         #If it's a comment, return an empty object if they don't have the right to see it
454         if ( $self->Type eq 'Comment' ) {
455             unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
456                 return ( $self->{'attachments'} );
457             }
458         }
459
460         #if they ain't got rights to see, return an empty object
461         elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
462             unless ( $self->CurrentUserHasRight('ShowTicket') ) {
463                 return ( $self->{'attachments'} );
464             }
465         }
466
467         $self->{'attachments'}->Limit( FIELD => 'TransactionId',
468                                        VALUE => $self->Id );
469
470         # Get the self->{'attachments'} in the order they're put into
471         # the database.  Arguably, we should be returning a tree
472         # of self->{'attachments'}, not a set...but no current app seems to need
473         # it.
474
475         $self->{'attachments'}->OrderBy( ALIAS => 'main',
476                                          FIELD => 'id',
477                                          ORDER => 'asc' );
478
479     }
480     return ( $self->{'attachments'} );
481
482 }
483
484 # }}}
485
486 # {{{ sub _Attach 
487
488 =head2 _Attach
489
490 A private method used to attach a mime object to this transaction.
491
492 =cut
493
494 sub _Attach {
495     my $self       = shift;
496     my $MIMEObject = shift;
497
498     if ( !defined($MIMEObject) ) {
499         $RT::Logger->error(
500 "$self _Attach: We can't attach a mime object if you don't give us one.\n"
501         );
502         return ( 0, $self->loc("[_1]: no attachment specified", $self) );
503     }
504
505     my $Attachment = new RT::Attachment( $self->CurrentUser );
506     my ($id, $msg) = $Attachment->Create(
507         TransactionId => $self->Id,
508         Attachment    => $MIMEObject
509     );
510     return ( $Attachment, $msg || $self->loc("Attachment created") );
511
512 }
513
514 # }}}
515
516 # }}}
517
518 # {{{ Routines dealing with Transaction Attributes
519
520 # {{{ sub Description 
521
522 =head2 Description
523
524 Returns a text string which describes this transaction
525
526 =cut
527
528 sub Description {
529     my $self = shift;
530
531     #Check those ACLs
532     #If it's a comment or a comment email record,
533     #  we need to be extra special careful
534
535     if ( $self->__Value('Type') =~ /^Comment/ ) {
536         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
537             return ( $self->loc("Permission Denied") );
538         }
539     }
540
541     #if they ain't got rights to see, don't let em
542     elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
543         unless ( $self->CurrentUserHasRight('ShowTicket') ) {
544             return ($self->loc("Permission Denied") );
545         }
546     }
547
548     if ( !defined( $self->Type ) ) {
549         return ( $self->loc("No transaction type specified"));
550     }
551
552     return ( $self->loc("[_1] by [_2]",$self->BriefDescription , $self->CreatorObj->Name ));
553 }
554
555 # }}}
556
557 # {{{ sub BriefDescription 
558
559 =head2 BriefDescription
560
561 Returns a text string which briefly describes this transaction
562
563 =cut
564
565 sub BriefDescription {
566     my $self = shift;
567
568     #If it's a comment or a comment email record,
569     #  we need to be extra special careful
570     if ( $self->__Value('Type') =~ /^Comment/ ) {
571         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
572             return ( $self->loc("Permission Denied") );
573         }
574     }
575
576     #if they ain't got rights to see, don't let em
577     elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
578         unless ( $self->CurrentUserHasRight('ShowTicket') ) {
579             return ( $self->loc("Permission Denied") );
580         }
581     }
582
583     my $type = $self->Type;    #cache this, rather than calling it 30 times
584
585     if ( !defined($type) ) {
586         return $self->loc("No transaction type specified");
587     }
588
589     my $obj_type = $self->FriendlyObjectType;
590
591     if ( $type eq 'Create' ) {
592         return ( $self->loc( "[_1] created", $obj_type ) );
593     }
594     elsif ( $type =~ /Status/ ) {
595         if ( $self->Field eq 'Status' ) {
596             if ( $self->NewValue eq 'deleted' ) {
597                 return ( $self->loc( "[_1] deleted", $obj_type ) );
598             }
599             else {
600                 return (
601                     $self->loc(
602                         "Status changed from [_1] to [_2]",
603                         "'" . $self->loc( $self->OldValue ) . "'",
604                         "'" . $self->loc( $self->NewValue ) . "'"
605                     )
606                 );
607
608             }
609         }
610
611         # Generic:
612         my $no_value = $self->loc("(no value)");
613         return (
614             $self->loc(
615                 "[_1] changed from [_2] to [_3]",
616                 $self->Field,
617                 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
618                 "'" . $self->NewValue . "'"
619             )
620         );
621     }
622
623     if ( my $code = $_BriefDescriptions{$type} ) {
624         return $code->($self);
625     }
626
627     return $self->loc(
628         "Default: [_1]/[_2] changed from [_3] to [_4]",
629         $type,
630         $self->Field,
631         (
632             $self->OldValue
633             ? "'" . $self->OldValue . "'"
634             : $self->loc("(no value)")
635         ),
636         "'" . $self->NewValue . "'"
637     );
638 }
639
640 %_BriefDescriptions = (
641     CommentEmailRecord => sub {
642         my $self = shift;
643         return $self->loc("Outgoing email about a comment recorded");
644     },
645     EmailRecord => sub {
646         my $self = shift;
647         return $self->loc("Outgoing email recorded");
648     },
649     Correspond => sub {
650         my $self = shift;
651         return $self->loc("Correspondence added");
652     },
653     Comment => sub {
654         my $self = shift;
655         return $self->loc("Comments added");
656     },
657     CustomField => sub {
658         my $self = shift;
659         my $field = $self->loc('CustomField');
660
661         if ( $self->Field ) {
662             my $cf = RT::CustomField->new( $self->CurrentUser );
663             $cf->Load( $self->Field );
664             $field = $cf->Name();
665         }
666
667         if ( $self->OldValue eq '' ) {
668             return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
669         }
670         elsif ( $self->NewValue eq '' ) {
671             return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
672
673         }
674         else {
675             return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
676         }
677     },
678     Untake => sub {
679         my $self = shift;
680         return $self->loc("Untaken");
681     },
682     Take => sub {
683         my $self = shift;
684         return $self->loc("Taken");
685     },
686     Force => sub {
687         my $self = shift;
688         my $Old = RT::User->new( $self->CurrentUser );
689         $Old->Load( $self->OldValue );
690         my $New = RT::User->new( $self->CurrentUser );
691         $New->Load( $self->NewValue );
692
693         return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
694     },
695     Steal => sub {
696         my $self = shift;
697         my $Old = RT::User->new( $self->CurrentUser );
698         $Old->Load( $self->OldValue );
699         return $self->loc("Stolen from [_1]",  $Old->Name);
700     },
701     Give => sub {
702         my $self = shift;
703         my $New = RT::User->new( $self->CurrentUser );
704         $New->Load( $self->NewValue );
705         return $self->loc( "Given to [_1]",  $New->Name );
706     },
707     AddWatcher => sub {
708         my $self = shift;
709         my $principal = RT::Principal->new($self->CurrentUser);
710         $principal->Load($self->NewValue);
711         return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
712     },
713     DelWatcher => sub {
714         my $self = shift;
715         my $principal = RT::Principal->new($self->CurrentUser);
716         $principal->Load($self->OldValue);
717         return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
718     },
719     Subject => sub {
720         my $self = shift;
721         return $self->loc( "Subject changed to [_1]", $self->Data );
722     },
723     AddLink => sub {
724         my $self = shift;
725         my $value;
726         if ( $self->NewValue ) {
727             my $URI = RT::URI->new( $self->CurrentUser );
728             $URI->FromURI( $self->NewValue );
729             if ( $URI->Resolver ) {
730                 $value = $URI->Resolver->AsString;
731             }
732             else {
733                 $value = $self->NewValue;
734             }
735             if ( $self->Field eq 'DependsOn' ) {
736                 return $self->loc( "Dependency on [_1] added", $value );
737             }
738             elsif ( $self->Field eq 'DependedOnBy' ) {
739                 return $self->loc( "Dependency by [_1] added", $value );
740
741             }
742             elsif ( $self->Field eq 'RefersTo' ) {
743                 return $self->loc( "Reference to [_1] added", $value );
744             }
745             elsif ( $self->Field eq 'ReferredToBy' ) {
746                 return $self->loc( "Reference by [_1] added", $value );
747             }
748             elsif ( $self->Field eq 'MemberOf' ) {
749                 return $self->loc( "Membership in [_1] added", $value );
750             }
751             elsif ( $self->Field eq 'HasMember' ) {
752                 return $self->loc( "Member [_1] added", $value );
753             }
754             elsif ( $self->Field eq 'MergedInto' ) {
755                 return $self->loc( "Merged into [_1]", $value );
756             }
757         }
758         else {
759             return ( $self->Data );
760         }
761     },
762     DeleteLink => sub {
763         my $self = shift;
764         my $value;
765         if ( $self->OldValue ) {
766             my $URI = RT::URI->new( $self->CurrentUser );
767             $URI->FromURI( $self->OldValue );
768             if ( $URI->Resolver ) {
769                 $value = $URI->Resolver->AsString;
770             }
771             else {
772                 $value = $self->OldValue;
773             }
774
775             if ( $self->Field eq 'DependsOn' ) {
776                 return $self->loc( "Dependency on [_1] deleted", $value );
777             }
778             elsif ( $self->Field eq 'DependedOnBy' ) {
779                 return $self->loc( "Dependency by [_1] deleted", $value );
780
781             }
782             elsif ( $self->Field eq 'RefersTo' ) {
783                 return $self->loc( "Reference to [_1] deleted", $value );
784             }
785             elsif ( $self->Field eq 'ReferredToBy' ) {
786                 return $self->loc( "Reference by [_1] deleted", $value );
787             }
788             elsif ( $self->Field eq 'MemberOf' ) {
789                 return $self->loc( "Membership in [_1] deleted", $value );
790             }
791             elsif ( $self->Field eq 'HasMember' ) {
792                 return $self->loc( "Member [_1] deleted", $value );
793             }
794         }
795         else {
796             return ( $self->Data );
797         }
798     },
799     Set => sub {
800         my $self = shift;
801         if ( $self->Field eq 'Password' ) {
802             return $self->loc('Password changed');
803         }
804         elsif ( $self->Field eq 'Queue' ) {
805             my $q1 = new RT::Queue( $self->CurrentUser );
806             $q1->Load( $self->OldValue );
807             my $q2 = new RT::Queue( $self->CurrentUser );
808             $q2->Load( $self->NewValue );
809             return $self->loc("[_1] changed from [_2] to [_3]", $self->Field , $q1->Name , $q2->Name);
810         }
811
812         # Write the date/time change at local time:
813         elsif ($self->Field =~  /Due|Starts|Started|Told/) {
814             my $t1 = new RT::Date($self->CurrentUser);
815             $t1->Set(Format => 'ISO', Value => $self->NewValue);
816             my $t2 = new RT::Date($self->CurrentUser);
817             $t2->Set(Format => 'ISO', Value => $self->OldValue);
818             return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, $t2->AsString, $t1->AsString );
819         }
820         else {
821             return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
822         }
823     },
824     PurgeTransaction => sub {
825         my $self = shift;
826         return $self->loc("Transaction [_1] purged", $self->Data);
827     },
828     AddReminder => sub {
829         my $self = shift;
830         my $ticket = RT::Ticket->new($self->CurrentUser);
831         $ticket->Load($self->NewValue);
832         return $self->loc("Reminder '[_1]' added", $ticket->Subject);
833     },
834     OpenReminder => sub {
835         my $self = shift;
836         my $ticket = RT::Ticket->new($self->CurrentUser);
837         $ticket->Load($self->NewValue);
838         return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
839     
840     },
841     ResolveReminder => sub {
842         my $self = shift;
843         my $ticket = RT::Ticket->new($self->CurrentUser);
844         $ticket->Load($self->NewValue);
845         return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
846     
847     
848     }
849 );
850
851 # }}}
852
853 # {{{ Utility methods
854
855 # {{{ sub IsInbound
856
857 =head2 IsInbound
858
859 Returns true if the creator of the transaction is a requestor of the ticket.
860 Returns false otherwise
861
862 =cut
863
864 sub IsInbound {
865     my $self = shift;
866     $self->ObjectType eq 'RT::Ticket' or return undef;
867     return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
868 }
869
870 # }}}
871
872 # }}}
873
874 sub _OverlayAccessible {
875     {
876
877           ObjectType => { public => 1},
878           ObjectId => { public => 1},
879
880     }
881 };
882
883 # }}}
884
885 # }}}
886
887 # {{{ sub _Set
888
889 sub _Set {
890     my $self = shift;
891     return ( 0, $self->loc('Transactions are immutable') );
892 }
893
894 # }}}
895
896 # {{{ sub _Value 
897
898 =head2 _Value
899
900 Takes the name of a table column.
901 Returns its value as a string, if the user passes an ACL check
902
903 =cut
904
905 sub _Value {
906
907     my $self  = shift;
908     my $field = shift;
909
910     #if the field is public, return it.
911     if ( $self->_Accessible( $field, 'public' ) ) {
912         return ( $self->__Value($field) );
913
914     }
915
916     #If it's a comment, we need to be extra special careful
917     if ( $self->__Value('Type') eq 'Comment' ) {
918         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
919             return (undef);
920         }
921     }
922     elsif ( $self->__Value('Type') eq 'CommentEmailRecord' ) {
923         unless ( $self->CurrentUserHasRight('ShowTicketComments')
924             && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
925             return (undef);
926         }
927
928     }
929     elsif ( $self->__Value('Type') eq 'EmailRecord' ) {
930         unless ( $self->CurrentUserHasRight('ShowOutgoingEmail')) {
931             return (undef);
932         }
933
934     }
935     # Make sure the user can see the custom field before showing that it changed
936     elsif ( ( $self->__Value('Type') eq 'CustomField' ) && $self->__Value('Field') ) {
937         my $cf = RT::CustomField->new( $self->CurrentUser );
938         $cf->Load( $self->__Value('Field') );
939         return (undef) unless ( $cf->CurrentUserHasRight('SeeCustomField') );
940     }
941
942
943     #if they ain't got rights to see, don't let em
944     elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
945         unless ( $self->CurrentUserHasRight('ShowTicket') ) {
946             return (undef);
947         }
948     }
949
950     return ( $self->__Value($field) );
951
952 }
953
954 # }}}
955
956 # {{{ sub CurrentUserHasRight
957
958 =head2 CurrentUserHasRight RIGHT
959
960 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
961 passed in here.
962
963 =cut
964
965 sub CurrentUserHasRight {
966     my $self  = shift;
967     my $right = shift;
968     return (
969         $self->CurrentUser->HasRight(
970             Right     => "$right",
971             Object => $self->TicketObj
972           )
973     );
974 }
975
976 # }}}
977
978 sub Ticket {
979     my $self = shift;
980     return $self->ObjectId;
981 }
982
983 sub TicketObj {
984     my $self = shift;
985     return $self->Object;
986 }
987
988 sub OldValue {
989     my $self = shift;
990     if ( my $type = $self->__Value('ReferenceType')
991          and my $id = $self->__Value('OldReference') )
992     {
993         my $Object = $type->new($self->CurrentUser);
994         $Object->Load( $id );
995         return $Object->Content;
996     }
997     else {
998         return $self->__Value('OldValue');
999     }
1000 }
1001
1002 sub NewValue {
1003     my $self = shift;
1004     if ( my $type = $self->__Value('ReferenceType')
1005          and my $id = $self->__Value('NewReference') )
1006     {
1007         my $Object = $type->new($self->CurrentUser);
1008         $Object->Load( $id );
1009         return $Object->Content;
1010     }
1011     else {
1012         return $self->__Value('NewValue');
1013     }
1014 }
1015
1016 sub Object {
1017     my $self  = shift;
1018     my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1019     $Object->Load($self->__Value('ObjectId'));
1020     return($Object);
1021 }
1022
1023 sub FriendlyObjectType {
1024     my $self = shift;
1025     my $type = $self->ObjectType or return undef;
1026     $type =~ s/^RT:://;
1027     return $self->loc($type);
1028 }
1029
1030 =head2 UpdateCustomFields
1031     
1032     Takes a hash of 
1033
1034     CustomField-<<Id>> => Value
1035         or 
1036
1037     Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1038     this transaction's custom fields
1039
1040 =cut
1041
1042 sub UpdateCustomFields {
1043     my $self = shift;
1044     my %args = (@_);
1045
1046     # This method used to have an API that took a hash of a single
1047     # value "ARGSRef", which was a reference to a hash of arguments.
1048     # This was insane. The next few lines of code preserve that API
1049     # while giving us something saner.
1050        
1051
1052     # TODO: 3.6: DEPRECATE OLD API
1053
1054     my $args; 
1055
1056     if ($args{'ARGSRef'}) { 
1057         $args = $args{ARGSRef};
1058     } else {
1059         $args = \%args;
1060     }
1061
1062     foreach my $arg ( keys %$args ) {
1063         next
1064           unless ( $arg =~
1065             /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1066         next if $arg =~ /-Magic$/;
1067         my $cfid   = $1;
1068         my $values = $args->{$arg};
1069         foreach
1070           my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1071         {
1072             next unless length($value);
1073             $self->_AddCustomFieldValue(
1074                 Field             => $cfid,
1075                 Value             => $value,
1076                 RecordTransaction => 0,
1077             );
1078         }
1079     }
1080 }
1081
1082
1083
1084 =head2 CustomFieldValues
1085
1086  Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1087
1088  See L<RT::Record>
1089
1090 =cut
1091
1092 sub CustomFieldValues {
1093     my $self  = shift;
1094     my $field = shift;
1095
1096     if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1097
1098         unless ( defined $field && $field =~ /^\d+$/o ) {
1099             my $CFs = RT::CustomFields->new( $self->CurrentUser );
1100              $CFs->Limit( FIELD => 'Name', VALUE => $field);
1101             $CFs->LimitToLookupType($self->CustomFieldLookupType);
1102             $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1103             $field = $CFs->First->id if $CFs->First;
1104         }
1105     }
1106     return $self->SUPER::CustomFieldValues($field);
1107 }
1108
1109 # }}}
1110
1111 # {{{ sub CustomFieldLookupType
1112
1113 =head2 CustomFieldLookupType
1114
1115 Returns the RT::Transaction lookup type, which can 
1116 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1117
1118 =cut
1119
1120 # }}}
1121
1122 sub CustomFieldLookupType {
1123     "RT::Queue-RT::Ticket-RT::Transaction";
1124 }
1125
1126 # Transactions don't change. by adding this cache congif directiove, we don't lose pathalogically on long tickets.
1127 sub _CacheConfig {
1128   {
1129      'cache_p'        => 1,
1130      'fast_update_p'  => 1,
1131      'cache_for_sec'  => 6000,
1132   }
1133 }
1134 1;