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