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