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