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