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