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