da766c0fd260ef4975474bc0ba18cc7df8c3a7c7
[usit-rt.git] / lib / RT / Transaction.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =head1 NAME
50
51   RT::Transaction - RT's transaction object
52
53 =head1 SYNOPSIS
54
55   use RT::Transaction;
56
57
58 =head1 DESCRIPTION
59
60
61 Each RT::Transaction describes an atomic change to a ticket object 
62 or an update to an RT::Ticket object.
63 It can have arbitrary MIME attachments.
64
65
66 =head1 METHODS
67
68
69 =cut
70
71
72 package RT::Transaction;
73
74 use base 'RT::Record';
75 use strict;
76 use warnings;
77
78
79 use vars qw( %_BriefDescriptions $PreferredContentType );
80
81 use RT::Attachments;
82 use RT::Scrips;
83 use RT::Ruleset;
84
85 use HTML::FormatText;
86 use HTML::TreeBuilder;
87
88
89 sub Table {'Transactions'}
90
91 # {{{ sub Create 
92
93 =head2 Create
94
95 Create a new transaction.
96
97 This routine should _never_ be called by anything other than RT::Ticket. 
98 It should not be called 
99 from client code. Ever. Not ever.  If you do this, we will hunt you down and break your kneecaps.
100 Then the unpleasant stuff will start.
101
102 TODO: Document what gets passed to this
103
104 =cut
105
106 sub Create {
107     my $self = shift;
108     my %args = (
109         id             => undef,
110         TimeTaken      => 0,
111         Type           => 'undefined',
112         Data           => '',
113         Field          => undef,
114         OldValue       => undef,
115         NewValue       => undef,
116         MIMEObj        => undef,
117         ActivateScrips => 1,
118         CommitScrips   => 1,
119         ObjectType     => 'RT::Ticket',
120         ObjectId       => 0,
121         ReferenceType  => undef,
122         OldReference   => undef,
123         NewReference   => undef,
124         SquelchMailTo  => undef,
125         @_
126     );
127
128     $args{ObjectId} ||= $args{Ticket};
129
130     #if we didn't specify a ticket, we need to bail
131     unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
132         return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
133     }
134
135
136
137     #lets create our transaction
138     my %params = (
139         Type      => $args{'Type'},
140         Data      => $args{'Data'},
141         Field     => $args{'Field'},
142         OldValue  => $args{'OldValue'},
143         NewValue  => $args{'NewValue'},
144         Created   => $args{'Created'},
145         ObjectType => $args{'ObjectType'},
146         ObjectId => $args{'ObjectId'},
147         ReferenceType => $args{'ReferenceType'},
148         OldReference => $args{'OldReference'},
149         NewReference => $args{'NewReference'},
150     );
151
152     # Parameters passed in during an import that we probably don't want to touch, otherwise
153     foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) {
154         $params{$attr} = $args{$attr} if ($args{$attr});
155     }
156  
157     my $id = $self->SUPER::Create(%params);
158     $self->Load($id);
159     if ( defined $args{'MIMEObj'} ) {
160         my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
161         unless ( $id ) {
162             $RT::Logger->error("Couldn't add attachment: $msg");
163             return ( 0, $self->loc("Couldn't add attachment") );
164         }
165     }
166
167     $self->AddAttribute(
168         Name    => 'SquelchMailTo',
169         Content => RT::User->CanonicalizeEmailAddress($_)
170     ) for @{$args{'SquelchMailTo'} || []};
171
172     #Provide a way to turn off scrips if we need to
173         $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
174     if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
175        $self->{'scrips'} = RT::Scrips->new(RT->SystemUser);
176
177         $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id); 
178
179         $self->{'scrips'}->Prepare(
180             Stage       => 'TransactionCreate',
181             Type        => $args{'Type'},
182             Ticket      => $args{'ObjectId'},
183             Transaction => $self->id,
184         );
185
186        # Entry point of the rule system
187        my $ticket = RT::Ticket->new(RT->SystemUser);
188        $ticket->Load($args{'ObjectId'});
189        my $txn = RT::Transaction->new($RT::SystemUser);
190        $txn->Load($self->id);
191
192        my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
193             Stage       => 'TransactionCreate',
194             Type        => $args{'Type'},
195             TicketObj   => $ticket,
196             TransactionObj => $txn,
197        );
198
199         if ($args{'CommitScrips'} ) {
200             $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
201             $self->{'scrips'}->Commit();
202             RT::Ruleset->CommitRules($rules);
203         }
204     }
205
206     return ( $id, $self->loc("Transaction Created") );
207 }
208
209
210 =head2 Scrips
211
212 Returns the Scrips object for this transaction.
213 This routine is only useful on a freshly created transaction object.
214 Scrips do not get persisted to the database with transactions.
215
216
217 =cut
218
219
220 sub Scrips {
221     my $self = shift;
222     return($self->{'scrips'});
223 }
224
225
226 =head2 Rules
227
228 Returns the array of Rule objects for this transaction.
229 This routine is only useful on a freshly created transaction object.
230 Rules do not get persisted to the database with transactions.
231
232
233 =cut
234
235
236 sub Rules {
237     my $self = shift;
238     return($self->{'rules'});
239 }
240
241
242
243 =head2 Delete
244
245 Delete this transaction. Currently DOES NOT CHECK ACLS
246
247 =cut
248
249 sub Delete {
250     my $self = shift;
251
252
253     $RT::Handle->BeginTransaction();
254
255     my $attachments = $self->Attachments;
256
257     while (my $attachment = $attachments->Next) {
258         my ($id, $msg) = $attachment->Delete();
259         unless ($id) {
260             $RT::Handle->Rollback();
261             return($id, $self->loc("System Error: [_1]", $msg));
262         }
263     }
264     my ($id,$msg) = $self->SUPER::Delete();
265         unless ($id) {
266             $RT::Handle->Rollback();
267             return($id, $self->loc("System Error: [_1]", $msg));
268         }
269     $RT::Handle->Commit();
270     return ($id,$msg);
271 }
272
273
274
275
276 =head2 Message
277
278 Returns the L<RT::Attachments> object which contains the "top-level" object
279 attachment for this transaction.
280
281 =cut
282
283 sub Message {
284     my $self = shift;
285
286     # XXX: Where is ACL check?
287     
288     unless ( defined $self->{'message'} ) {
289
290         $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
291         $self->{'message'}->Limit(
292             FIELD => 'TransactionId',
293             VALUE => $self->Id
294         );
295         $self->{'message'}->ChildrenOf(0);
296     } else {
297         $self->{'message'}->GotoFirstItem;
298     }
299     return $self->{'message'};
300 }
301
302
303
304 =head2 Content PARAMHASH
305
306 If this transaction has attached mime objects, returns the body of the first
307 textual part (as defined in RT::I18N::IsTextualContentType).  Otherwise,
308 returns undef.
309
310 Takes a paramhash.  If the $args{'Quote'} parameter is set, wraps this message 
311 at $args{'Wrap'}.  $args{'Wrap'} defaults to 70.
312
313 If $args{'Type'} is set to C<text/html>, this will return an HTML 
314 part of the message, if available.  Otherwise it looks for a text/plain
315 part. If $args{'Type'} is missing, it defaults to the value of 
316 C<$RT::Transaction::PreferredContentType>, if that's missing too, 
317 defaults to textual.
318
319 =cut
320
321 sub Content {
322     my $self = shift;
323     my %args = (
324         Type => $PreferredContentType || '',
325         Quote => 0,
326         Wrap  => 70,
327         @_
328     );
329
330     my $content;
331     if ( my $content_obj =
332         $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
333     {
334         $content = $content_obj->Content ||'';
335
336         if ( lc $content_obj->ContentType eq 'text/html' ) {
337             $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
338
339             if ($args{Type} ne 'text/html') {
340                 my $tree = HTML::TreeBuilder->new_from_content( $content );
341                 $content = HTML::FormatText->new(
342                     leftmargin  => 0,
343                     rightmargin => 78,
344                 )->format( $tree);
345                 $tree->delete;
346             }
347         }
348         else {
349             $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
350             if ($args{Type} eq 'text/html') {
351                 # Extremely simple text->html converter
352                 $content =~ s/&/&#38;/g;
353                 $content =~ s/</&lt;/g;
354                 $content =~ s/>/&gt;/g;
355                 $content = "<pre>$content</pre>";
356             }
357         }
358     }
359
360     # If all else fails, return a message that we couldn't find any content
361     else {
362         $content = $self->loc('This transaction appears to have no content');
363     }
364
365     if ( $args{'Quote'} ) {
366
367         # What's the longest line like?
368         my $max = 0;
369         foreach ( split ( /\n/, $content ) ) {
370             $max = length if length > $max;
371         }
372
373         if ( $max > 76 ) {
374             require Text::Wrapper;
375             my $wrapper = Text::Wrapper->new(
376                 columns    => $args{'Wrap'},
377                 body_start => ( $max > 70 * 3 ? '   ' : '' ),
378                 par_start  => ''
379             );
380             $content = $wrapper->wrap($content);
381         }
382
383         $content =~ s/^/> /gm;
384         $content = $self->QuoteHeader . "\n$content\n\n";
385     }
386
387     return ($content);
388 }
389
390 =head2 QuoteHeader
391
392 Returns text prepended to content when transaction is quoted
393 (see C<Quote> argument in L</Content>). By default returns
394 localized "On <date> <user name> wrote:\n".
395
396 =cut
397
398 sub QuoteHeader {
399     my $self = shift;
400     return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
401 }
402
403
404 =head2 Addresses
405
406 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
407
408 =cut
409
410 sub Addresses {
411         my $self = shift;
412
413         if (my $attach = $self->Attachments->First) {   
414                 return $attach->Addresses;
415         }
416         else {
417                 return {};
418         }
419
420 }
421
422
423
424 =head2 ContentObj 
425
426 Returns the RT::Attachment object which contains the content for this Transaction
427
428 =cut
429
430
431 sub ContentObj {
432     my $self = shift;
433     my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
434
435     # If we don't have any content, return undef now.
436     # Get the set of toplevel attachments to this transaction.
437
438     my $Attachment = $args{'Attachment'};
439
440     $Attachment ||= $self->Attachments->First;
441
442     return undef unless ($Attachment);
443
444     # If it's a textual part, just return the body.
445     if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
446         return ($Attachment);
447     }
448
449     # If it's a multipart object, first try returning the first part with preferred
450     # MIME type ('text/plain' by default).
451
452     elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
453         my $kids = $Attachment->Children;
454         while (my $child = $kids->Next) {
455             my $ret =  $self->ContentObj(%args, Attachment => $child);
456             return $ret if ($ret);
457         }
458     }
459     elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
460         if ( $args{Type} ) {
461             my $plain_parts = $Attachment->Children;
462             $plain_parts->ContentType( VALUE => $args{Type} );
463             $plain_parts->LimitNotEmpty;
464
465             # If we actully found a part, return its content
466             if ( my $first = $plain_parts->First ) {
467                 return $first;
468             }
469         }
470
471         # If that fails, return the first textual part which has some content.
472         my $all_parts = $self->Attachments;
473         while ( my $part = $all_parts->Next ) {
474             next unless RT::I18N::IsTextualContentType($part->ContentType)
475                         && $part->Content;
476             return $part;
477         }
478     }
479
480     # We found no content. suck
481     return (undef);
482 }
483
484
485
486 =head2 Subject
487
488 If this transaction has attached mime objects, returns the first one's subject
489 Otherwise, returns null
490   
491 =cut
492
493 sub Subject {
494     my $self = shift;
495     return undef unless my $first = $self->Attachments->First;
496     return $first->Subject;
497 }
498
499
500
501 =head2 Attachments
502
503 Returns all the RT::Attachment objects which are attached
504 to this transaction. Takes an optional parameter, which is
505 a ContentType that Attachments should be restricted to.
506
507 =cut
508
509 sub Attachments {
510     my $self = shift;
511
512     if ( $self->{'attachments'} ) {
513         $self->{'attachments'}->GotoFirstItem;
514         return $self->{'attachments'};
515     }
516
517     $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
518
519     unless ( $self->CurrentUserCanSee ) {
520         $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
521         return $self->{'attachments'};
522     }
523
524     $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
525
526     # Get the self->{'attachments'} in the order they're put into
527     # the database.  Arguably, we should be returning a tree
528     # of self->{'attachments'}, not a set...but no current app seems to need
529     # it.
530
531     $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
532
533     return $self->{'attachments'};
534 }
535
536
537
538 =head2 _Attach
539
540 A private method used to attach a mime object to this transaction.
541
542 =cut
543
544 sub _Attach {
545     my $self       = shift;
546     my $MIMEObject = shift;
547
548     unless ( defined $MIMEObject ) {
549         $RT::Logger->error("We can't attach a mime object if you don't give us one.");
550         return ( 0, $self->loc("[_1]: no attachment specified", $self) );
551     }
552
553     my $Attachment = RT::Attachment->new( $self->CurrentUser );
554     my ($id, $msg) = $Attachment->Create(
555         TransactionId => $self->Id,
556         Attachment    => $MIMEObject
557     );
558     return ( $Attachment, $msg || $self->loc("Attachment created") );
559 }
560
561
562
563 sub ContentAsMIME {
564     my $self = shift;
565
566     # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
567     # since it has less information available without looking to it's parent
568     # transaction.  Check ACLs here before we go any further.
569     return unless $self->CurrentUserCanSee;
570
571     my $attachments = RT::Attachments->new( $self->CurrentUser );
572     $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
573     $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
574     $attachments->Limit( FIELD => 'Parent',        VALUE => 0 );
575     $attachments->RowsPerPage(1);
576
577     my $top = $attachments->First;
578     return unless $top;
579
580     my $entity = MIME::Entity->build(
581         Type        => 'message/rfc822',
582         Description => 'transaction ' . $self->id,
583         Data        => $top->ContentAsMIME(Children => 1)->as_string,
584     );
585
586     return $entity;
587 }
588
589
590
591 =head2 Description
592
593 Returns a text string which describes this transaction
594
595 =cut
596
597 sub Description {
598     my $self = shift;
599
600     unless ( $self->CurrentUserCanSee ) {
601         return ( $self->loc("Permission Denied") );
602     }
603
604     unless ( defined $self->Type ) {
605         return ( $self->loc("No transaction type specified"));
606     }
607
608     return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
609 }
610
611
612
613 =head2 BriefDescription
614
615 Returns a text string which briefly describes this transaction
616
617 =cut
618
619 sub BriefDescription {
620     my $self = shift;
621
622     unless ( $self->CurrentUserCanSee ) {
623         return ( $self->loc("Permission Denied") );
624     }
625
626     my $type = $self->Type;    #cache this, rather than calling it 30 times
627
628     unless ( defined $type ) {
629         return $self->loc("No transaction type specified");
630     }
631
632     my $obj_type = $self->FriendlyObjectType;
633
634     if ( $type eq 'Create' ) {
635         return ( $self->loc( "[_1] created", $obj_type ) );
636     }
637     elsif ( $type eq 'Enabled' ) {
638         return ( $self->loc( "[_1] enabled", $obj_type ) );
639     }
640     elsif ( $type eq 'Disabled' ) {
641         return ( $self->loc( "[_1] disabled", $obj_type ) );
642     }
643     elsif ( $type =~ /Status/ ) {
644         if ( $self->Field eq 'Status' ) {
645             if ( $self->NewValue eq 'deleted' ) {
646                 return ( $self->loc( "[_1] deleted", $obj_type ) );
647             }
648             else {
649                 my $canon = $self->Object->can("QueueObj")
650                     ? sub { $self->Object->QueueObj->Lifecycle->CanonicalCase(@_) }
651                     : sub { return $_[0] };
652                 return (
653                     $self->loc(
654                         "Status changed from [_1] to [_2]",
655                         "'" . $self->loc( $canon->($self->OldValue) ) . "'",
656                         "'" . $self->loc( $canon->($self->NewValue) ) . "'"
657                     )
658                 );
659
660             }
661         }
662
663         # Generic:
664         my $no_value = $self->loc("(no value)");
665         return (
666             $self->loc(
667                 "[_1] changed from [_2] to [_3]",
668                 $self->Field,
669                 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
670                 "'" . $self->NewValue . "'"
671             )
672         );
673     }
674     elsif ( $type =~ /SystemError/ ) {
675         return $self->loc("System error");
676     }
677     elsif ( $type =~ /Forward Transaction/ ) {
678         return $self->loc( "Forwarded Transaction #[_1] to [_2]",
679             $self->Field, $self->Data );
680     }
681     elsif ( $type =~ /Forward Ticket/ ) {
682         return $self->loc( "Forwarded Ticket to [_1]", $self->Data );
683     }
684
685     if ( my $code = $_BriefDescriptions{$type} ) {
686         return $code->($self);
687     }
688
689     return $self->loc(
690         "Default: [_1]/[_2] changed from [_3] to [_4]",
691         $type,
692         $self->Field,
693         (
694             $self->OldValue
695             ? "'" . $self->OldValue . "'"
696             : $self->loc("(no value)")
697         ),
698         "'" . $self->NewValue . "'"
699     );
700 }
701
702 %_BriefDescriptions = (
703     CommentEmailRecord => sub {
704         my $self = shift;
705         return $self->loc("Outgoing email about a comment recorded");
706     },
707     EmailRecord => sub {
708         my $self = shift;
709         return $self->loc("Outgoing email recorded");
710     },
711     Correspond => sub {
712         my $self = shift;
713         return $self->loc("Correspondence added");
714     },
715     Comment => sub {
716         my $self = shift;
717         return $self->loc("Comments added");
718     },
719     CustomField => sub {
720         my $self = shift;
721         my $field = $self->loc('CustomField');
722
723         if ( $self->Field ) {
724             my $cf = RT::CustomField->new( $self->CurrentUser );
725             $cf->SetContextObject( $self->Object );
726             $cf->Load( $self->Field );
727             $field = $cf->Name();
728             $field = $self->loc('a custom field') if !defined($field);
729         }
730
731         my $new = $self->NewValue;
732         my $old = $self->OldValue;
733
734         if ( !defined($old) || $old eq '' ) {
735             return $self->loc("[_1] [_2] added", $field, $new);
736         }
737         elsif ( !defined($new) || $new eq '' ) {
738             return $self->loc("[_1] [_2] deleted", $field, $old);
739         }
740         else {
741             return $self->loc("[_1] [_2] changed to [_3]", $field, $old, $new);
742         }
743     },
744     Untake => sub {
745         my $self = shift;
746         return $self->loc("Untaken");
747     },
748     Take => sub {
749         my $self = shift;
750         return $self->loc("Taken");
751     },
752     Force => sub {
753         my $self = shift;
754         my $Old = RT::User->new( $self->CurrentUser );
755         $Old->Load( $self->OldValue );
756         my $New = RT::User->new( $self->CurrentUser );
757         $New->Load( $self->NewValue );
758
759         return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
760     },
761     Steal => sub {
762         my $self = shift;
763         my $Old = RT::User->new( $self->CurrentUser );
764         $Old->Load( $self->OldValue );
765         return $self->loc("Stolen from [_1]",  $Old->Name);
766     },
767     Give => sub {
768         my $self = shift;
769         my $New = RT::User->new( $self->CurrentUser );
770         $New->Load( $self->NewValue );
771         return $self->loc( "Given to [_1]",  $New->Name );
772     },
773     AddWatcher => sub {
774         my $self = shift;
775         my $principal = RT::Principal->new($self->CurrentUser);
776         $principal->Load($self->NewValue);
777         return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
778     },
779     DelWatcher => sub {
780         my $self = shift;
781         my $principal = RT::Principal->new($self->CurrentUser);
782         $principal->Load($self->OldValue);
783         return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
784     },
785     Subject => sub {
786         my $self = shift;
787         return $self->loc( "Subject changed to [_1]", $self->Data );
788     },
789     AddLink => sub {
790         my $self = shift;
791         my $value;
792         if ( $self->NewValue ) {
793             my $URI = RT::URI->new( $self->CurrentUser );
794             if ( $URI->FromURI( $self->NewValue ) ) {
795                 $value = $URI->Resolver->AsString;
796             }
797             else {
798                 $value = $self->NewValue;
799             }
800             if ( $self->Field eq 'DependsOn' ) {
801                 return $self->loc( "Dependency on [_1] added", $value );
802             }
803             elsif ( $self->Field eq 'DependedOnBy' ) {
804                 return $self->loc( "Dependency by [_1] added", $value );
805
806             }
807             elsif ( $self->Field eq 'RefersTo' ) {
808                 return $self->loc( "Reference to [_1] added", $value );
809             }
810             elsif ( $self->Field eq 'ReferredToBy' ) {
811                 return $self->loc( "Reference by [_1] added", $value );
812             }
813             elsif ( $self->Field eq 'MemberOf' ) {
814                 return $self->loc( "Membership in [_1] added", $value );
815             }
816             elsif ( $self->Field eq 'HasMember' ) {
817                 return $self->loc( "Member [_1] added", $value );
818             }
819             elsif ( $self->Field eq 'MergedInto' ) {
820                 return $self->loc( "Merged into [_1]", $value );
821             }
822         }
823         else {
824             return ( $self->Data );
825         }
826     },
827     DeleteLink => sub {
828         my $self = shift;
829         my $value;
830         if ( $self->OldValue ) {
831             my $URI = RT::URI->new( $self->CurrentUser );
832             if ( $URI->FromURI( $self->OldValue ) ){
833                 $value = $URI->Resolver->AsString;
834             }
835             else {
836                 $value = $self->OldValue;
837             }
838
839             if ( $self->Field eq 'DependsOn' ) {
840                 return $self->loc( "Dependency on [_1] deleted", $value );
841             }
842             elsif ( $self->Field eq 'DependedOnBy' ) {
843                 return $self->loc( "Dependency by [_1] deleted", $value );
844
845             }
846             elsif ( $self->Field eq 'RefersTo' ) {
847                 return $self->loc( "Reference to [_1] deleted", $value );
848             }
849             elsif ( $self->Field eq 'ReferredToBy' ) {
850                 return $self->loc( "Reference by [_1] deleted", $value );
851             }
852             elsif ( $self->Field eq 'MemberOf' ) {
853                 return $self->loc( "Membership in [_1] deleted", $value );
854             }
855             elsif ( $self->Field eq 'HasMember' ) {
856                 return $self->loc( "Member [_1] deleted", $value );
857             }
858         }
859         else {
860             return ( $self->Data );
861         }
862     },
863     Told => sub {
864         my $self = shift;
865         if ( $self->Field eq 'Told' ) {
866             my $t1 = RT::Date->new($self->CurrentUser);
867             $t1->Set(Format => 'ISO', Value => $self->NewValue);
868             my $t2 = RT::Date->new($self->CurrentUser);
869             $t2->Set(Format => 'ISO', Value => $self->OldValue);
870             return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
871         }
872         else {
873             return $self->loc( "[_1] changed from [_2] to [_3]",
874                                $self->loc($self->Field),
875                                ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
876         }
877     },
878     Set => sub {
879         my $self = shift;
880         if ( $self->Field eq 'Password' ) {
881             return $self->loc('Password changed');
882         }
883         elsif ( $self->Field eq 'Queue' ) {
884             my $q1 = RT::Queue->new( $self->CurrentUser );
885             $q1->Load( $self->OldValue );
886             my $q2 = RT::Queue->new( $self->CurrentUser );
887             $q2->Load( $self->NewValue );
888             return $self->loc("[_1] changed from [_2] to [_3]",
889                               $self->loc($self->Field) , $q1->Name , $q2->Name);
890         }
891
892         # Write the date/time change at local time:
893         elsif ($self->Field =~  /Due|Starts|Started|Told/) {
894             my $t1 = RT::Date->new($self->CurrentUser);
895             $t1->Set(Format => 'ISO', Value => $self->NewValue);
896             my $t2 = RT::Date->new($self->CurrentUser);
897             $t2->Set(Format => 'ISO', Value => $self->OldValue);
898             return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
899         }
900         elsif ( $self->Field eq 'Owner' ) {
901             my $Old = RT::User->new( $self->CurrentUser );
902             $Old->Load( $self->OldValue );
903             my $New = RT::User->new( $self->CurrentUser );
904             $New->Load( $self->NewValue );
905
906             if ( $Old->id == RT->Nobody->id ) {
907                 if ( $New->id == $self->Creator ) {
908                     return $self->loc("Taken");
909                 }
910                 else {
911                     return $self->loc( "Given to [_1]",  $New->Name );
912                 }
913             }
914             else {
915                 if ( $New->id == $self->Creator ) {
916                     return $self->loc("Stolen from [_1]",  $Old->Name);
917                 }
918                 elsif ( $Old->id == $self->Creator ) {
919                     if ( $New->id == RT->Nobody->id ) {
920                         return $self->loc("Untaken");
921                     }
922                     else {
923                         return $self->loc( "Given to [_1]", $New->Name );
924                     }
925                 }
926                 else {
927                     return $self->loc(
928                         "Owner forcibly changed from [_1] to [_2]",
929                         $Old->Name, $New->Name );
930                 }
931             }
932         }
933         else {
934             return $self->loc( "[_1] changed from [_2] to [_3]",
935                                $self->loc($self->Field),
936                                ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
937         }
938     },
939     PurgeTransaction => sub {
940         my $self = shift;
941         return $self->loc("Transaction [_1] purged", $self->Data);
942     },
943     AddReminder => sub {
944         my $self = shift;
945         my $ticket = RT::Ticket->new($self->CurrentUser);
946         $ticket->Load($self->NewValue);
947         return $self->loc("Reminder '[_1]' added", $ticket->Subject);
948     },
949     OpenReminder => sub {
950         my $self = shift;
951         my $ticket = RT::Ticket->new($self->CurrentUser);
952         $ticket->Load($self->NewValue);
953         return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
954     
955     },
956     ResolveReminder => sub {
957         my $self = shift;
958         my $ticket = RT::Ticket->new($self->CurrentUser);
959         $ticket->Load($self->NewValue);
960         return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
961     
962     
963     }
964 );
965
966
967
968
969 =head2 IsInbound
970
971 Returns true if the creator of the transaction is a requestor of the ticket.
972 Returns false otherwise
973
974 =cut
975
976 sub IsInbound {
977     my $self = shift;
978     $self->ObjectType eq 'RT::Ticket' or return undef;
979     return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
980 }
981
982
983
984 sub _OverlayAccessible {
985     {
986
987           ObjectType => { public => 1},
988           ObjectId => { public => 1},
989
990     }
991 };
992
993
994
995
996 sub _Set {
997     my $self = shift;
998     return ( 0, $self->loc('Transactions are immutable') );
999 }
1000
1001
1002
1003 =head2 _Value
1004
1005 Takes the name of a table column.
1006 Returns its value as a string, if the user passes an ACL check
1007
1008 =cut
1009
1010 sub _Value {
1011     my $self  = shift;
1012     my $field = shift;
1013
1014     #if the field is public, return it.
1015     if ( $self->_Accessible( $field, 'public' ) ) {
1016         return $self->SUPER::_Value( $field );
1017     }
1018
1019     unless ( $self->CurrentUserCanSee ) {
1020         return undef;
1021     }
1022
1023     return $self->SUPER::_Value( $field );
1024 }
1025
1026
1027
1028 =head2 CurrentUserHasRight RIGHT
1029
1030 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1031 passed in here.
1032
1033 =cut
1034
1035 sub CurrentUserHasRight {
1036     my $self  = shift;
1037     my $right = shift;
1038     return $self->CurrentUser->HasRight(
1039         Right  => $right,
1040         Object => $self->Object
1041     );
1042 }
1043
1044 =head2 CurrentUserCanSee
1045
1046 Returns true if current user has rights to see this particular transaction.
1047
1048 This fact depends on type of the transaction, type of an object the transaction
1049 is attached to and may be other conditions, so this method is prefered over
1050 custom implementations.
1051
1052 =cut
1053
1054 sub CurrentUserCanSee {
1055     my $self = shift;
1056
1057     # If it's a comment, we need to be extra special careful
1058     my $type = $self->__Value('Type');
1059     if ( $type eq 'Comment' ) {
1060         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1061             return 0;
1062         }
1063     }
1064     elsif ( $type eq 'CommentEmailRecord' ) {
1065         unless ( $self->CurrentUserHasRight('ShowTicketComments')
1066             && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1067             return 0;
1068         }
1069     }
1070     elsif ( $type eq 'EmailRecord' ) {
1071         unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1072             return 0;
1073         }
1074     }
1075     # Make sure the user can see the custom field before showing that it changed
1076     elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1077         my $cf = RT::CustomField->new( $self->CurrentUser );
1078         $cf->SetContextObject( $self->Object );
1079         $cf->Load( $cf_id );
1080         return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1081     }
1082
1083     # Transactions that might have changed the ->Object's visibility to
1084     # the current user are marked readable
1085     return 1 if $self->{ _object_is_readable };
1086
1087     # Defer to the object in question
1088     return $self->Object->CurrentUserCanSee("Transaction");
1089 }
1090
1091
1092 sub Ticket {
1093     my $self = shift;
1094     return $self->ObjectId;
1095 }
1096
1097 sub TicketObj {
1098     my $self = shift;
1099     return $self->Object;
1100 }
1101
1102 sub OldValue {
1103     my $self = shift;
1104     if ( my $type = $self->__Value('ReferenceType')
1105          and my $id = $self->__Value('OldReference') )
1106     {
1107         my $Object = $type->new($self->CurrentUser);
1108         $Object->Load( $id );
1109         return $Object->Content;
1110     }
1111     else {
1112         return $self->_Value('OldValue');
1113     }
1114 }
1115
1116 sub NewValue {
1117     my $self = shift;
1118     if ( my $type = $self->__Value('ReferenceType')
1119          and my $id = $self->__Value('NewReference') )
1120     {
1121         my $Object = $type->new($self->CurrentUser);
1122         $Object->Load( $id );
1123         return $Object->Content;
1124     }
1125     else {
1126         return $self->_Value('NewValue');
1127     }
1128 }
1129
1130 sub Object {
1131     my $self  = shift;
1132     my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1133     $Object->Load($self->__Value('ObjectId'));
1134     return $Object;
1135 }
1136
1137 sub FriendlyObjectType {
1138     my $self = shift;
1139     my $type = $self->ObjectType or return undef;
1140     $type =~ s/^RT:://;
1141     return $self->loc($type);
1142 }
1143
1144 =head2 UpdateCustomFields
1145     
1146     Takes a hash of 
1147
1148     CustomField-<<Id>> => Value
1149         or 
1150
1151     Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1152     this transaction's custom fields
1153
1154 =cut
1155
1156 sub UpdateCustomFields {
1157     my $self = shift;
1158     my %args = (@_);
1159
1160     # This method used to have an API that took a hash of a single
1161     # value "ARGSRef", which was a reference to a hash of arguments.
1162     # This was insane. The next few lines of code preserve that API
1163     # while giving us something saner.
1164
1165     # TODO: 3.6: DEPRECATE OLD API
1166
1167     my $args; 
1168
1169     if ($args{'ARGSRef'}) { 
1170         $args = $args{ARGSRef};
1171     } else {
1172         $args = \%args;
1173     }
1174
1175     foreach my $arg ( keys %$args ) {
1176         next
1177           unless ( $arg =~
1178             /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1179         next if $arg =~ /-Magic$/;
1180         my $cfid   = $1;
1181         my $values = $args->{$arg};
1182         foreach
1183           my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1184         {
1185             next unless (defined($value) && length($value));
1186             $self->_AddCustomFieldValue(
1187                 Field             => $cfid,
1188                 Value             => $value,
1189                 RecordTransaction => 0,
1190             );
1191         }
1192     }
1193 }
1194
1195 =head2 LoadCustomFieldByIdentifier
1196
1197 Finds and returns the custom field of the given name for the
1198 transaction, overriding L<RT::Record/LoadCustomFieldByIdentifier> to
1199 look for queue-specific CFs before global ones.
1200
1201 =cut
1202
1203 sub LoadCustomFieldByIdentifier {
1204     my $self  = shift;
1205     my $field = shift;
1206
1207     return $self->SUPER::LoadCustomFieldByIdentifier($field)
1208         if ref $field or $field =~ /^\d+$/;
1209
1210     return $self->SUPER::LoadCustomFieldByIdentifier($field)
1211         unless UNIVERSAL::can( $self->Object, 'QueueObj' );
1212
1213     my $CFs = RT::CustomFields->new( $self->CurrentUser );
1214     $CFs->SetContextObject( $self->Object );
1215     $CFs->Limit( FIELD => 'Name', VALUE => $field );
1216     $CFs->LimitToLookupType($self->CustomFieldLookupType);
1217     $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1218     return $CFs->First || RT::CustomField->new( $self->CurrentUser );
1219 }
1220
1221 =head2 CustomFieldLookupType
1222
1223 Returns the RT::Transaction lookup type, which can 
1224 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1225
1226 =cut
1227
1228
1229 sub CustomFieldLookupType {
1230     "RT::Queue-RT::Ticket-RT::Transaction";
1231 }
1232
1233
1234 =head2 SquelchMailTo
1235
1236 Similar to Ticket class SquelchMailTo method - returns a list of
1237 transaction's squelched addresses.  As transactions are immutable, the
1238 list of squelched recipients cannot be modified after creation.
1239
1240 =cut
1241
1242 sub SquelchMailTo {
1243     my $self = shift;
1244     return () unless $self->CurrentUserCanSee;
1245     return $self->Attributes->Named('SquelchMailTo');
1246 }
1247
1248 =head2 Recipients
1249
1250 Returns the list of email addresses (as L<Email::Address> objects)
1251 that this transaction would send mail to.  There may be duplicates.
1252
1253 =cut
1254
1255 sub Recipients {
1256     my $self = shift;
1257     my @recipients;
1258     foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1259         my $action = $scrip->ActionObj->Action;
1260         next unless $action->isa('RT::Action::SendEmail');
1261
1262         foreach my $type (qw(To Cc Bcc)) {
1263             push @recipients, $action->$type();
1264         }
1265     }
1266
1267     if ( $self->Rules ) {
1268         for my $rule (@{$self->Rules}) {
1269             next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1270             my $data = $rule->{hints}{recipients};
1271             foreach my $type (qw(To Cc Bcc)) {
1272                 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1273             }
1274         }
1275     }
1276     return @recipients;
1277 }
1278
1279 =head2 DeferredRecipients($freq, $include_sent )
1280
1281 Takes the following arguments:
1282
1283 =over
1284
1285 =item * a string to indicate the frequency of digest delivery.  Valid values are "daily", "weekly", or "susp".
1286
1287 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1288
1289 =back
1290
1291 Returns an array of users who should now receive the notification that
1292 was recorded in this transaction.  Returns an empty array if there were
1293 no deferred users, or if $include_sent was not specified and the deferred
1294 notifications have been sent.
1295
1296 =cut
1297
1298 sub DeferredRecipients {
1299     my $self = shift;
1300     my $freq = shift;
1301     my $include_sent = @_? shift : 0;
1302
1303     my $attr = $self->FirstAttribute('DeferredRecipients');
1304
1305     return () unless ($attr);
1306
1307     my $deferred = $attr->Content;
1308
1309     return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1310
1311     # Skip it.
1312    
1313     for my $user (keys %{$deferred->{$freq}}) {
1314         if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) { 
1315             delete $deferred->{$freq}->{$user} 
1316         }
1317     }
1318     # Now get our users.  Easy.
1319     
1320     return keys %{ $deferred->{$freq} };
1321 }
1322
1323
1324
1325 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1326 sub _CacheConfig {
1327   {
1328      'cache_p'        => 1,
1329      'fast_update_p'  => 1,
1330      'cache_for_sec'  => 6000,
1331   }
1332 }
1333
1334
1335 =head2 ACLEquivalenceObjects
1336
1337 This method returns a list of objects for which a user's rights also apply
1338 to this Transaction.
1339
1340 This currently only applies to Transaction Custom Fields on Tickets, so we return
1341 the Ticket's Queue and the Ticket.
1342
1343 This method is called from L<RT::Principal/HasRight>.
1344
1345 =cut
1346
1347 sub ACLEquivalenceObjects {
1348     my $self = shift;
1349
1350     return unless $self->ObjectType eq 'RT::Ticket';
1351     my $object = $self->Object;
1352     return $object,$object->QueueObj;
1353
1354 }
1355
1356
1357
1358
1359
1360 =head2 id
1361
1362 Returns the current value of id.
1363 (In the database, id is stored as int(11).)
1364
1365
1366 =cut
1367
1368
1369 =head2 ObjectType
1370
1371 Returns the current value of ObjectType.
1372 (In the database, ObjectType is stored as varchar(64).)
1373
1374
1375
1376 =head2 SetObjectType VALUE
1377
1378
1379 Set ObjectType to VALUE.
1380 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1381 (In the database, ObjectType will be stored as a varchar(64).)
1382
1383
1384 =cut
1385
1386
1387 =head2 ObjectId
1388
1389 Returns the current value of ObjectId.
1390 (In the database, ObjectId is stored as int(11).)
1391
1392
1393
1394 =head2 SetObjectId VALUE
1395
1396
1397 Set ObjectId to VALUE.
1398 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1399 (In the database, ObjectId will be stored as a int(11).)
1400
1401
1402 =cut
1403
1404
1405 =head2 TimeTaken
1406
1407 Returns the current value of TimeTaken.
1408 (In the database, TimeTaken is stored as int(11).)
1409
1410
1411
1412 =head2 SetTimeTaken VALUE
1413
1414
1415 Set TimeTaken to VALUE.
1416 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1417 (In the database, TimeTaken will be stored as a int(11).)
1418
1419
1420 =cut
1421
1422
1423 =head2 Type
1424
1425 Returns the current value of Type.
1426 (In the database, Type is stored as varchar(20).)
1427
1428
1429
1430 =head2 SetType VALUE
1431
1432
1433 Set Type to VALUE.
1434 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1435 (In the database, Type will be stored as a varchar(20).)
1436
1437
1438 =cut
1439
1440
1441 =head2 Field
1442
1443 Returns the current value of Field.
1444 (In the database, Field is stored as varchar(40).)
1445
1446
1447
1448 =head2 SetField VALUE
1449
1450
1451 Set Field to VALUE.
1452 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1453 (In the database, Field will be stored as a varchar(40).)
1454
1455
1456 =cut
1457
1458
1459 =head2 OldValue
1460
1461 Returns the current value of OldValue.
1462 (In the database, OldValue is stored as varchar(255).)
1463
1464
1465
1466 =head2 SetOldValue VALUE
1467
1468
1469 Set OldValue to VALUE.
1470 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1471 (In the database, OldValue will be stored as a varchar(255).)
1472
1473
1474 =cut
1475
1476
1477 =head2 NewValue
1478
1479 Returns the current value of NewValue.
1480 (In the database, NewValue is stored as varchar(255).)
1481
1482
1483
1484 =head2 SetNewValue VALUE
1485
1486
1487 Set NewValue to VALUE.
1488 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1489 (In the database, NewValue will be stored as a varchar(255).)
1490
1491
1492 =cut
1493
1494
1495 =head2 ReferenceType
1496
1497 Returns the current value of ReferenceType.
1498 (In the database, ReferenceType is stored as varchar(255).)
1499
1500
1501
1502 =head2 SetReferenceType VALUE
1503
1504
1505 Set ReferenceType to VALUE.
1506 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1507 (In the database, ReferenceType will be stored as a varchar(255).)
1508
1509
1510 =cut
1511
1512
1513 =head2 OldReference
1514
1515 Returns the current value of OldReference.
1516 (In the database, OldReference is stored as int(11).)
1517
1518
1519
1520 =head2 SetOldReference VALUE
1521
1522
1523 Set OldReference to VALUE.
1524 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1525 (In the database, OldReference will be stored as a int(11).)
1526
1527
1528 =cut
1529
1530
1531 =head2 NewReference
1532
1533 Returns the current value of NewReference.
1534 (In the database, NewReference is stored as int(11).)
1535
1536
1537
1538 =head2 SetNewReference VALUE
1539
1540
1541 Set NewReference to VALUE.
1542 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1543 (In the database, NewReference will be stored as a int(11).)
1544
1545
1546 =cut
1547
1548
1549 =head2 Data
1550
1551 Returns the current value of Data.
1552 (In the database, Data is stored as varchar(255).)
1553
1554
1555
1556 =head2 SetData VALUE
1557
1558
1559 Set Data to VALUE.
1560 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1561 (In the database, Data will be stored as a varchar(255).)
1562
1563
1564 =cut
1565
1566
1567 =head2 Creator
1568
1569 Returns the current value of Creator.
1570 (In the database, Creator is stored as int(11).)
1571
1572
1573 =cut
1574
1575
1576 =head2 Created
1577
1578 Returns the current value of Created.
1579 (In the database, Created is stored as datetime.)
1580
1581
1582 =cut
1583
1584
1585
1586 sub _CoreAccessible {
1587     {
1588
1589         id =>
1590                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1591         ObjectType =>
1592                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
1593         ObjectId =>
1594                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1595         TimeTaken =>
1596                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1597         Type =>
1598                 {read => 1, write => 1, sql_type => 12, length => 20,  is_blob => 0,  is_numeric => 0,  type => 'varchar(20)', default => ''},
1599         Field =>
1600                 {read => 1, write => 1, sql_type => 12, length => 40,  is_blob => 0,  is_numeric => 0,  type => 'varchar(40)', default => ''},
1601         OldValue =>
1602                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1603         NewValue =>
1604                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1605         ReferenceType =>
1606                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1607         OldReference =>
1608                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1609         NewReference =>
1610                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1611         Data =>
1612                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1613         Creator =>
1614                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1615         Created =>
1616                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
1617
1618  }
1619 };
1620
1621 RT::Base->_ImportOverlays();
1622
1623 1;