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