Dev to 4.0.11
[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                 return (
650                     $self->loc(
651                         "Status changed from [_1] to [_2]",
652                         "'" . $self->loc( $self->OldValue ) . "'",
653                         "'" . $self->loc( $self->NewValue ) . "'"
654                     )
655                 );
656
657             }
658         }
659
660         # Generic:
661         my $no_value = $self->loc("(no value)");
662         return (
663             $self->loc(
664                 "[_1] changed from [_2] to [_3]",
665                 $self->Field,
666                 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
667                 "'" . $self->NewValue . "'"
668             )
669         );
670     }
671     elsif ( $type =~ /SystemError/ ) {
672         return $self->loc("System error");
673     }
674     elsif ( $type =~ /Forward Transaction/ ) {
675         return $self->loc( "Forwarded Transaction #[_1] to [_2]",
676             $self->Field, $self->Data );
677     }
678     elsif ( $type =~ /Forward Ticket/ ) {
679         return $self->loc( "Forwarded Ticket to [_1]", $self->Data );
680     }
681
682     if ( my $code = $_BriefDescriptions{$type} ) {
683         return $code->($self);
684     }
685
686     return $self->loc(
687         "Default: [_1]/[_2] changed from [_3] to [_4]",
688         $type,
689         $self->Field,
690         (
691             $self->OldValue
692             ? "'" . $self->OldValue . "'"
693             : $self->loc("(no value)")
694         ),
695         "'" . $self->NewValue . "'"
696     );
697 }
698
699 %_BriefDescriptions = (
700     CommentEmailRecord => sub {
701         my $self = shift;
702         return $self->loc("Outgoing email about a comment recorded");
703     },
704     EmailRecord => sub {
705         my $self = shift;
706         return $self->loc("Outgoing email recorded");
707     },
708     Correspond => sub {
709         my $self = shift;
710         return $self->loc("Correspondence added");
711     },
712     Comment => sub {
713         my $self = shift;
714         return $self->loc("Comments added");
715     },
716     CustomField => sub {
717         my $self = shift;
718         my $field = $self->loc('CustomField');
719
720         if ( $self->Field ) {
721             my $cf = RT::CustomField->new( $self->CurrentUser );
722             $cf->SetContextObject( $self->Object );
723             $cf->Load( $self->Field );
724             $field = $cf->Name();
725             $field = $self->loc('a custom field') if !defined($field);
726         }
727
728         my $new = $self->NewValue;
729         my $old = $self->OldValue;
730
731         if ( !defined($old) || $old eq '' ) {
732             return $self->loc("[_1] [_2] added", $field, $new);
733         }
734         elsif ( !defined($new) || $new eq '' ) {
735             return $self->loc("[_1] [_2] deleted", $field, $old);
736         }
737         else {
738             return $self->loc("[_1] [_2] changed to [_3]", $field, $old, $new);
739         }
740     },
741     Untake => sub {
742         my $self = shift;
743         return $self->loc("Untaken");
744     },
745     Take => sub {
746         my $self = shift;
747         return $self->loc("Taken");
748     },
749     Force => sub {
750         my $self = shift;
751         my $Old = RT::User->new( $self->CurrentUser );
752         $Old->Load( $self->OldValue );
753         my $New = RT::User->new( $self->CurrentUser );
754         $New->Load( $self->NewValue );
755
756         return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
757     },
758     Steal => sub {
759         my $self = shift;
760         my $Old = RT::User->new( $self->CurrentUser );
761         $Old->Load( $self->OldValue );
762         return $self->loc("Stolen from [_1]",  $Old->Name);
763     },
764     Give => sub {
765         my $self = shift;
766         my $New = RT::User->new( $self->CurrentUser );
767         $New->Load( $self->NewValue );
768         return $self->loc( "Given to [_1]",  $New->Name );
769     },
770     AddWatcher => sub {
771         my $self = shift;
772         my $principal = RT::Principal->new($self->CurrentUser);
773         $principal->Load($self->NewValue);
774         return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
775     },
776     DelWatcher => sub {
777         my $self = shift;
778         my $principal = RT::Principal->new($self->CurrentUser);
779         $principal->Load($self->OldValue);
780         return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
781     },
782     Subject => sub {
783         my $self = shift;
784         return $self->loc( "Subject changed to [_1]", $self->Data );
785     },
786     AddLink => sub {
787         my $self = shift;
788         my $value;
789         if ( $self->NewValue ) {
790             my $URI = RT::URI->new( $self->CurrentUser );
791             if ( $URI->FromURI( $self->NewValue ) ) {
792                 $value = $URI->Resolver->AsString;
793             }
794             else {
795                 $value = $self->NewValue;
796             }
797             if ( $self->Field eq 'DependsOn' ) {
798                 return $self->loc( "Dependency on [_1] added", $value );
799             }
800             elsif ( $self->Field eq 'DependedOnBy' ) {
801                 return $self->loc( "Dependency by [_1] added", $value );
802
803             }
804             elsif ( $self->Field eq 'RefersTo' ) {
805                 return $self->loc( "Reference to [_1] added", $value );
806             }
807             elsif ( $self->Field eq 'ReferredToBy' ) {
808                 return $self->loc( "Reference by [_1] added", $value );
809             }
810             elsif ( $self->Field eq 'MemberOf' ) {
811                 return $self->loc( "Membership in [_1] added", $value );
812             }
813             elsif ( $self->Field eq 'HasMember' ) {
814                 return $self->loc( "Member [_1] added", $value );
815             }
816             elsif ( $self->Field eq 'MergedInto' ) {
817                 return $self->loc( "Merged into [_1]", $value );
818             }
819         }
820         else {
821             return ( $self->Data );
822         }
823     },
824     DeleteLink => sub {
825         my $self = shift;
826         my $value;
827         if ( $self->OldValue ) {
828             my $URI = RT::URI->new( $self->CurrentUser );
829             if ( $URI->FromURI( $self->OldValue ) ){
830                 $value = $URI->Resolver->AsString;
831             }
832             else {
833                 $value = $self->OldValue;
834             }
835
836             if ( $self->Field eq 'DependsOn' ) {
837                 return $self->loc( "Dependency on [_1] deleted", $value );
838             }
839             elsif ( $self->Field eq 'DependedOnBy' ) {
840                 return $self->loc( "Dependency by [_1] deleted", $value );
841
842             }
843             elsif ( $self->Field eq 'RefersTo' ) {
844                 return $self->loc( "Reference to [_1] deleted", $value );
845             }
846             elsif ( $self->Field eq 'ReferredToBy' ) {
847                 return $self->loc( "Reference by [_1] deleted", $value );
848             }
849             elsif ( $self->Field eq 'MemberOf' ) {
850                 return $self->loc( "Membership in [_1] deleted", $value );
851             }
852             elsif ( $self->Field eq 'HasMember' ) {
853                 return $self->loc( "Member [_1] deleted", $value );
854             }
855         }
856         else {
857             return ( $self->Data );
858         }
859     },
860     Told => sub {
861         my $self = shift;
862         if ( $self->Field eq 'Told' ) {
863             my $t1 = RT::Date->new($self->CurrentUser);
864             $t1->Set(Format => 'ISO', Value => $self->NewValue);
865             my $t2 = RT::Date->new($self->CurrentUser);
866             $t2->Set(Format => 'ISO', Value => $self->OldValue);
867             return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
868         }
869         else {
870             return $self->loc( "[_1] changed from [_2] to [_3]",
871                                $self->loc($self->Field),
872                                ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
873         }
874     },
875     Set => sub {
876         my $self = shift;
877         if ( $self->Field eq 'Password' ) {
878             return $self->loc('Password changed');
879         }
880         elsif ( $self->Field eq 'Queue' ) {
881             my $q1 = RT::Queue->new( $self->CurrentUser );
882             $q1->Load( $self->OldValue );
883             my $q2 = RT::Queue->new( $self->CurrentUser );
884             $q2->Load( $self->NewValue );
885             return $self->loc("[_1] changed from [_2] to [_3]",
886                               $self->loc($self->Field) , $q1->Name , $q2->Name);
887         }
888
889         # Write the date/time change at local time:
890         elsif ($self->Field =~  /Due|Starts|Started|Told/) {
891             my $t1 = RT::Date->new($self->CurrentUser);
892             $t1->Set(Format => 'ISO', Value => $self->NewValue);
893             my $t2 = RT::Date->new($self->CurrentUser);
894             $t2->Set(Format => 'ISO', Value => $self->OldValue);
895             return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
896         }
897         elsif ( $self->Field eq 'Owner' ) {
898             my $Old = RT::User->new( $self->CurrentUser );
899             $Old->Load( $self->OldValue );
900             my $New = RT::User->new( $self->CurrentUser );
901             $New->Load( $self->NewValue );
902
903             if ( $Old->id == RT->Nobody->id ) {
904                 if ( $New->id == $self->Creator ) {
905                     return $self->loc("Taken");
906                 }
907                 else {
908                     return $self->loc( "Given to [_1]",  $New->Name );
909                 }
910             }
911             else {
912                 if ( $New->id == $self->Creator ) {
913                     return $self->loc("Stolen from [_1]",  $Old->Name);
914                 }
915                 elsif ( $Old->id == $self->Creator ) {
916                     if ( $New->id == RT->Nobody->id ) {
917                         return $self->loc("Untaken");
918                     }
919                     else {
920                         return $self->loc( "Given to [_1]", $New->Name );
921                     }
922                 }
923                 else {
924                     return $self->loc(
925                         "Owner forcibly changed from [_1] to [_2]",
926                         $Old->Name, $New->Name );
927                 }
928             }
929         }
930         else {
931             return $self->loc( "[_1] changed from [_2] to [_3]",
932                                $self->loc($self->Field),
933                                ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
934         }
935     },
936     PurgeTransaction => sub {
937         my $self = shift;
938         return $self->loc("Transaction [_1] purged", $self->Data);
939     },
940     AddReminder => sub {
941         my $self = shift;
942         my $ticket = RT::Ticket->new($self->CurrentUser);
943         $ticket->Load($self->NewValue);
944         return $self->loc("Reminder '[_1]' added", $ticket->Subject);
945     },
946     OpenReminder => sub {
947         my $self = shift;
948         my $ticket = RT::Ticket->new($self->CurrentUser);
949         $ticket->Load($self->NewValue);
950         return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
951     
952     },
953     ResolveReminder => sub {
954         my $self = shift;
955         my $ticket = RT::Ticket->new($self->CurrentUser);
956         $ticket->Load($self->NewValue);
957         return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
958     
959     
960     }
961 );
962
963
964
965
966 =head2 IsInbound
967
968 Returns true if the creator of the transaction is a requestor of the ticket.
969 Returns false otherwise
970
971 =cut
972
973 sub IsInbound {
974     my $self = shift;
975     $self->ObjectType eq 'RT::Ticket' or return undef;
976     return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
977 }
978
979
980
981 sub _OverlayAccessible {
982     {
983
984           ObjectType => { public => 1},
985           ObjectId => { public => 1},
986
987     }
988 };
989
990
991
992
993 sub _Set {
994     my $self = shift;
995     return ( 0, $self->loc('Transactions are immutable') );
996 }
997
998
999
1000 =head2 _Value
1001
1002 Takes the name of a table column.
1003 Returns its value as a string, if the user passes an ACL check
1004
1005 =cut
1006
1007 sub _Value {
1008     my $self  = shift;
1009     my $field = shift;
1010
1011     #if the field is public, return it.
1012     if ( $self->_Accessible( $field, 'public' ) ) {
1013         return $self->SUPER::_Value( $field );
1014     }
1015
1016     unless ( $self->CurrentUserCanSee ) {
1017         return undef;
1018     }
1019
1020     return $self->SUPER::_Value( $field );
1021 }
1022
1023
1024
1025 =head2 CurrentUserHasRight RIGHT
1026
1027 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1028 passed in here.
1029
1030 =cut
1031
1032 sub CurrentUserHasRight {
1033     my $self  = shift;
1034     my $right = shift;
1035     return $self->CurrentUser->HasRight(
1036         Right  => $right,
1037         Object => $self->Object
1038     );
1039 }
1040
1041 =head2 CurrentUserCanSee
1042
1043 Returns true if current user has rights to see this particular transaction.
1044
1045 This fact depends on type of the transaction, type of an object the transaction
1046 is attached to and may be other conditions, so this method is prefered over
1047 custom implementations.
1048
1049 =cut
1050
1051 sub CurrentUserCanSee {
1052     my $self = shift;
1053
1054     # If it's a comment, we need to be extra special careful
1055     my $type = $self->__Value('Type');
1056     if ( $type eq 'Comment' ) {
1057         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1058             return 0;
1059         }
1060     }
1061     elsif ( $type eq 'CommentEmailRecord' ) {
1062         unless ( $self->CurrentUserHasRight('ShowTicketComments')
1063             && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1064             return 0;
1065         }
1066     }
1067     elsif ( $type eq 'EmailRecord' ) {
1068         unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1069             return 0;
1070         }
1071     }
1072     # Make sure the user can see the custom field before showing that it changed
1073     elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1074         my $cf = RT::CustomField->new( $self->CurrentUser );
1075         $cf->SetContextObject( $self->Object );
1076         $cf->Load( $cf_id );
1077         return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1078     }
1079
1080     # Transactions that might have changed the ->Object's visibility to
1081     # the current user are marked readable
1082     return 1 if $self->{ _object_is_readable };
1083
1084     # Defer to the object in question
1085     return $self->Object->CurrentUserCanSee("Transaction");
1086 }
1087
1088
1089 sub Ticket {
1090     my $self = shift;
1091     return $self->ObjectId;
1092 }
1093
1094 sub TicketObj {
1095     my $self = shift;
1096     return $self->Object;
1097 }
1098
1099 sub OldValue {
1100     my $self = shift;
1101     if ( my $type = $self->__Value('ReferenceType')
1102          and my $id = $self->__Value('OldReference') )
1103     {
1104         my $Object = $type->new($self->CurrentUser);
1105         $Object->Load( $id );
1106         return $Object->Content;
1107     }
1108     else {
1109         return $self->_Value('OldValue');
1110     }
1111 }
1112
1113 sub NewValue {
1114     my $self = shift;
1115     if ( my $type = $self->__Value('ReferenceType')
1116          and my $id = $self->__Value('NewReference') )
1117     {
1118         my $Object = $type->new($self->CurrentUser);
1119         $Object->Load( $id );
1120         return $Object->Content;
1121     }
1122     else {
1123         return $self->_Value('NewValue');
1124     }
1125 }
1126
1127 sub Object {
1128     my $self  = shift;
1129     my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1130     $Object->Load($self->__Value('ObjectId'));
1131     return $Object;
1132 }
1133
1134 sub FriendlyObjectType {
1135     my $self = shift;
1136     my $type = $self->ObjectType or return undef;
1137     $type =~ s/^RT:://;
1138     return $self->loc($type);
1139 }
1140
1141 =head2 UpdateCustomFields
1142     
1143     Takes a hash of 
1144
1145     CustomField-<<Id>> => Value
1146         or 
1147
1148     Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1149     this transaction's custom fields
1150
1151 =cut
1152
1153 sub UpdateCustomFields {
1154     my $self = shift;
1155     my %args = (@_);
1156
1157     # This method used to have an API that took a hash of a single
1158     # value "ARGSRef", which was a reference to a hash of arguments.
1159     # This was insane. The next few lines of code preserve that API
1160     # while giving us something saner.
1161
1162     # TODO: 3.6: DEPRECATE OLD API
1163
1164     my $args; 
1165
1166     if ($args{'ARGSRef'}) { 
1167         $args = $args{ARGSRef};
1168     } else {
1169         $args = \%args;
1170     }
1171
1172     foreach my $arg ( keys %$args ) {
1173         next
1174           unless ( $arg =~
1175             /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1176         next if $arg =~ /-Magic$/;
1177         my $cfid   = $1;
1178         my $values = $args->{$arg};
1179         foreach
1180           my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1181         {
1182             next unless (defined($value) && length($value));
1183             $self->_AddCustomFieldValue(
1184                 Field             => $cfid,
1185                 Value             => $value,
1186                 RecordTransaction => 0,
1187             );
1188         }
1189     }
1190 }
1191
1192 =head2 LoadCustomFieldByIdentifier
1193
1194 Finds and returns the custom field of the given name for the
1195 transaction, overriding L<RT::Record/LoadCustomFieldByIdentifier> to
1196 look for queue-specific CFs before global ones.
1197
1198 =cut
1199
1200 sub LoadCustomFieldByIdentifier {
1201     my $self  = shift;
1202     my $field = shift;
1203
1204     return $self->SUPER::LoadCustomFieldByIdentifier($field)
1205         if ref $field or $field =~ /^\d+$/;
1206
1207     return $self->SUPER::LoadCustomFieldByIdentifier($field)
1208         unless UNIVERSAL::can( $self->Object, 'QueueObj' );
1209
1210     my $CFs = RT::CustomFields->new( $self->CurrentUser );
1211     $CFs->SetContextObject( $self->Object );
1212     $CFs->Limit( FIELD => 'Name', VALUE => $field );
1213     $CFs->LimitToLookupType($self->CustomFieldLookupType);
1214     $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1215     return $CFs->First || RT::CustomField->new( $self->CurrentUser );
1216 }
1217
1218 =head2 CustomFieldLookupType
1219
1220 Returns the RT::Transaction lookup type, which can 
1221 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1222
1223 =cut
1224
1225
1226 sub CustomFieldLookupType {
1227     "RT::Queue-RT::Ticket-RT::Transaction";
1228 }
1229
1230
1231 =head2 SquelchMailTo
1232
1233 Similar to Ticket class SquelchMailTo method - returns a list of
1234 transaction's squelched addresses.  As transactions are immutable, the
1235 list of squelched recipients cannot be modified after creation.
1236
1237 =cut
1238
1239 sub SquelchMailTo {
1240     my $self = shift;
1241     return () unless $self->CurrentUserCanSee;
1242     return $self->Attributes->Named('SquelchMailTo');
1243 }
1244
1245 =head2 Recipients
1246
1247 Returns the list of email addresses (as L<Email::Address> objects)
1248 that this transaction would send mail to.  There may be duplicates.
1249
1250 =cut
1251
1252 sub Recipients {
1253     my $self = shift;
1254     my @recipients;
1255     foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1256         my $action = $scrip->ActionObj->Action;
1257         next unless $action->isa('RT::Action::SendEmail');
1258
1259         foreach my $type (qw(To Cc Bcc)) {
1260             push @recipients, $action->$type();
1261         }
1262     }
1263
1264     if ( $self->Rules ) {
1265         for my $rule (@{$self->Rules}) {
1266             next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1267             my $data = $rule->{hints}{recipients};
1268             foreach my $type (qw(To Cc Bcc)) {
1269                 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1270             }
1271         }
1272     }
1273     return @recipients;
1274 }
1275
1276 =head2 DeferredRecipients($freq, $include_sent )
1277
1278 Takes the following arguments:
1279
1280 =over
1281
1282 =item * a string to indicate the frequency of digest delivery.  Valid values are "daily", "weekly", or "susp".
1283
1284 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1285
1286 =back
1287
1288 Returns an array of users who should now receive the notification that
1289 was recorded in this transaction.  Returns an empty array if there were
1290 no deferred users, or if $include_sent was not specified and the deferred
1291 notifications have been sent.
1292
1293 =cut
1294
1295 sub DeferredRecipients {
1296     my $self = shift;
1297     my $freq = shift;
1298     my $include_sent = @_? shift : 0;
1299
1300     my $attr = $self->FirstAttribute('DeferredRecipients');
1301
1302     return () unless ($attr);
1303
1304     my $deferred = $attr->Content;
1305
1306     return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1307
1308     # Skip it.
1309    
1310     for my $user (keys %{$deferred->{$freq}}) {
1311         if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) { 
1312             delete $deferred->{$freq}->{$user} 
1313         }
1314     }
1315     # Now get our users.  Easy.
1316     
1317     return keys %{ $deferred->{$freq} };
1318 }
1319
1320
1321
1322 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1323 sub _CacheConfig {
1324   {
1325      'cache_p'        => 1,
1326      'fast_update_p'  => 1,
1327      'cache_for_sec'  => 6000,
1328   }
1329 }
1330
1331
1332 =head2 ACLEquivalenceObjects
1333
1334 This method returns a list of objects for which a user's rights also apply
1335 to this Transaction.
1336
1337 This currently only applies to Transaction Custom Fields on Tickets, so we return
1338 the Ticket's Queue and the Ticket.
1339
1340 This method is called from L<RT::Principal/HasRight>.
1341
1342 =cut
1343
1344 sub ACLEquivalenceObjects {
1345     my $self = shift;
1346
1347     return unless $self->ObjectType eq 'RT::Ticket';
1348     my $object = $self->Object;
1349     return $object,$object->QueueObj;
1350
1351 }
1352
1353
1354
1355
1356
1357 =head2 id
1358
1359 Returns the current value of id.
1360 (In the database, id is stored as int(11).)
1361
1362
1363 =cut
1364
1365
1366 =head2 ObjectType
1367
1368 Returns the current value of ObjectType.
1369 (In the database, ObjectType is stored as varchar(64).)
1370
1371
1372
1373 =head2 SetObjectType VALUE
1374
1375
1376 Set ObjectType to VALUE.
1377 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1378 (In the database, ObjectType will be stored as a varchar(64).)
1379
1380
1381 =cut
1382
1383
1384 =head2 ObjectId
1385
1386 Returns the current value of ObjectId.
1387 (In the database, ObjectId is stored as int(11).)
1388
1389
1390
1391 =head2 SetObjectId VALUE
1392
1393
1394 Set ObjectId to VALUE.
1395 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1396 (In the database, ObjectId will be stored as a int(11).)
1397
1398
1399 =cut
1400
1401
1402 =head2 TimeTaken
1403
1404 Returns the current value of TimeTaken.
1405 (In the database, TimeTaken is stored as int(11).)
1406
1407
1408
1409 =head2 SetTimeTaken VALUE
1410
1411
1412 Set TimeTaken to VALUE.
1413 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1414 (In the database, TimeTaken will be stored as a int(11).)
1415
1416
1417 =cut
1418
1419
1420 =head2 Type
1421
1422 Returns the current value of Type.
1423 (In the database, Type is stored as varchar(20).)
1424
1425
1426
1427 =head2 SetType VALUE
1428
1429
1430 Set Type to VALUE.
1431 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1432 (In the database, Type will be stored as a varchar(20).)
1433
1434
1435 =cut
1436
1437
1438 =head2 Field
1439
1440 Returns the current value of Field.
1441 (In the database, Field is stored as varchar(40).)
1442
1443
1444
1445 =head2 SetField VALUE
1446
1447
1448 Set Field to VALUE.
1449 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1450 (In the database, Field will be stored as a varchar(40).)
1451
1452
1453 =cut
1454
1455
1456 =head2 OldValue
1457
1458 Returns the current value of OldValue.
1459 (In the database, OldValue is stored as varchar(255).)
1460
1461
1462
1463 =head2 SetOldValue VALUE
1464
1465
1466 Set OldValue to VALUE.
1467 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1468 (In the database, OldValue will be stored as a varchar(255).)
1469
1470
1471 =cut
1472
1473
1474 =head2 NewValue
1475
1476 Returns the current value of NewValue.
1477 (In the database, NewValue is stored as varchar(255).)
1478
1479
1480
1481 =head2 SetNewValue VALUE
1482
1483
1484 Set NewValue to VALUE.
1485 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1486 (In the database, NewValue will be stored as a varchar(255).)
1487
1488
1489 =cut
1490
1491
1492 =head2 ReferenceType
1493
1494 Returns the current value of ReferenceType.
1495 (In the database, ReferenceType is stored as varchar(255).)
1496
1497
1498
1499 =head2 SetReferenceType VALUE
1500
1501
1502 Set ReferenceType to VALUE.
1503 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1504 (In the database, ReferenceType will be stored as a varchar(255).)
1505
1506
1507 =cut
1508
1509
1510 =head2 OldReference
1511
1512 Returns the current value of OldReference.
1513 (In the database, OldReference is stored as int(11).)
1514
1515
1516
1517 =head2 SetOldReference VALUE
1518
1519
1520 Set OldReference to VALUE.
1521 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1522 (In the database, OldReference will be stored as a int(11).)
1523
1524
1525 =cut
1526
1527
1528 =head2 NewReference
1529
1530 Returns the current value of NewReference.
1531 (In the database, NewReference is stored as int(11).)
1532
1533
1534
1535 =head2 SetNewReference VALUE
1536
1537
1538 Set NewReference to VALUE.
1539 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1540 (In the database, NewReference will be stored as a int(11).)
1541
1542
1543 =cut
1544
1545
1546 =head2 Data
1547
1548 Returns the current value of Data.
1549 (In the database, Data is stored as varchar(255).)
1550
1551
1552
1553 =head2 SetData VALUE
1554
1555
1556 Set Data to VALUE.
1557 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1558 (In the database, Data will be stored as a varchar(255).)
1559
1560
1561 =cut
1562
1563
1564 =head2 Creator
1565
1566 Returns the current value of Creator.
1567 (In the database, Creator is stored as int(11).)
1568
1569
1570 =cut
1571
1572
1573 =head2 Created
1574
1575 Returns the current value of Created.
1576 (In the database, Created is stored as datetime.)
1577
1578
1579 =cut
1580
1581
1582
1583 sub _CoreAccessible {
1584     {
1585
1586         id =>
1587                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1588         ObjectType =>
1589                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
1590         ObjectId =>
1591                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1592         TimeTaken =>
1593                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1594         Type =>
1595                 {read => 1, write => 1, sql_type => 12, length => 20,  is_blob => 0,  is_numeric => 0,  type => 'varchar(20)', default => ''},
1596         Field =>
1597                 {read => 1, write => 1, sql_type => 12, length => 40,  is_blob => 0,  is_numeric => 0,  type => 'varchar(40)', default => ''},
1598         OldValue =>
1599                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1600         NewValue =>
1601                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1602         ReferenceType =>
1603                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1604         OldReference =>
1605                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1606         NewReference =>
1607                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1608         Data =>
1609                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1610         Creator =>
1611                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1612         Created =>
1613                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
1614
1615  }
1616 };
1617
1618 RT::Base->_ImportOverlays();
1619
1620 1;