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