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