import rt 3.6.6
[freeside.git] / rt / lib / RT / Record.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
6 #                                          <jesse@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/copyleft/gpl.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 =head1 NAME
49
50   RT::Record - Base class for RT record objects
51
52 =head1 SYNOPSIS
53
54
55 =head1 DESCRIPTION
56
57
58 =begin testing
59
60 ok (require RT::Record);
61
62 =end testing
63
64 =head1 METHODS
65
66 =cut
67
68 package RT::Record;
69
70 use strict;
71 use warnings;
72
73 our @ISA;
74 use base qw(RT::Base);
75
76 use RT::Date;
77 use RT::User;
78 use RT::Attributes;
79 use DBIx::SearchBuilder::Record::Cachable;
80 use Encode qw();
81
82 our $_TABLE_ATTR = { };
83
84
85 if ( $RT::DontCacheSearchBuilderRecords ) {
86     push (@ISA, 'DBIx::SearchBuilder::Record');
87 } else {
88     push (@ISA, 'DBIx::SearchBuilder::Record::Cachable');
89
90 }
91
92 # {{{ sub _Init 
93
94 sub _Init {
95     my $self = shift;
96     $self->_BuildTableAttributes unless ($_TABLE_ATTR->{ref($self)});
97     $self->CurrentUser(@_);
98 }
99
100 # }}}
101
102 # {{{ _PrimaryKeys
103
104 =head2 _PrimaryKeys
105
106 The primary keys for RT classes is 'id'
107
108 =cut
109
110 sub _PrimaryKeys {
111     my $self = shift;
112     return ( ['id'] );
113 }
114
115 # }}}
116
117 =head2 Delete
118
119 Delete this record object from the database.
120
121 =cut
122
123 sub Delete {
124     my $self = shift;
125     my ($rv) = $self->SUPER::Delete;
126     if ($rv) {
127         return ($rv, $self->loc("Object deleted"));
128     } else {
129
130         return(0, $self->loc("Object could not be deleted"))
131     } 
132 }
133
134 =head2 ObjectTypeStr
135
136 Returns a string which is this object's type.  The type is the class,
137 without the "RT::" prefix.
138
139 =begin testing
140
141 my $ticket = RT::Ticket->new($RT::SystemUser);
142 my $group = RT::Group->new($RT::SystemUser);
143 is($ticket->ObjectTypeStr, 'Ticket', "Ticket returns correct typestring");
144 is($group->ObjectTypeStr, 'Group', "Group returns correct typestring");
145
146 =end testing
147
148 =cut
149
150 sub ObjectTypeStr {
151     my $self = shift;
152     if (ref($self) =~ /^.*::(\w+)$/) {
153         return $self->loc($1);
154     } else {
155         return $self->loc(ref($self));
156     }
157 }
158
159 =head2 Attributes
160
161 Return this object's attributes as an RT::Attributes object
162
163 =cut
164
165 sub Attributes {
166     my $self = shift;
167     
168     unless ($self->{'attributes'}) {
169         $self->{'attributes'} = RT::Attributes->new($self->CurrentUser);     
170        $self->{'attributes'}->LimitToObject($self); 
171     }
172     return ($self->{'attributes'}); 
173
174 }
175
176
177 =head2 AddAttribute { Name, Description, Content }
178
179 Adds a new attribute for this object.
180
181 =cut
182
183 sub AddAttribute {
184     my $self = shift;
185     my %args = ( Name        => undef,
186                  Description => undef,
187                  Content     => undef,
188                  @_ );
189
190     my $attr = RT::Attribute->new( $self->CurrentUser );
191     my ( $id, $msg ) = $attr->Create( 
192                                       Object    => $self,
193                                       Name        => $args{'Name'},
194                                       Description => $args{'Description'},
195                                       Content     => $args{'Content'} );
196
197
198     # XXX TODO: Why won't RedoSearch work here?                                     
199     $self->Attributes->_DoSearch;
200     
201     return ($id, $msg);
202 }
203
204
205 =head2 SetAttribute { Name, Description, Content }
206
207 Like AddAttribute, but replaces all existing attributes with the same Name.
208
209 =cut
210
211 sub SetAttribute {
212     my $self = shift;
213     my %args = ( Name        => undef,
214                  Description => undef,
215                  Content     => undef,
216                  @_ );
217
218     my @AttributeObjs = $self->Attributes->Named( $args{'Name'} )
219         or return $self->AddAttribute( %args );
220
221     my $AttributeObj = pop( @AttributeObjs );
222     $_->Delete foreach @AttributeObjs;
223
224     $AttributeObj->SetDescription( $args{'Description'} );
225     $AttributeObj->SetContent( $args{'Content'} );
226
227     $self->Attributes->RedoSearch;
228     return 1;
229 }
230
231 =head2 DeleteAttribute NAME
232
233 Deletes all attributes with the matching name for this object.
234
235 =cut
236
237 sub DeleteAttribute {
238     my $self = shift;
239     my $name = shift;
240     return $self->Attributes->DeleteEntry( Name => $name );
241 }
242
243 =head2 FirstAttribute NAME
244
245 Returns the first attribute with the matching name for this object (as an
246 L<RT::Attribute> object), or C<undef> if no such attributes exist.
247
248 Note that if there is more than one attribute with the matching name on the
249 object, the choice of which one to return is basically arbitrary.  This may be
250 made well-defined in the future.
251
252 =cut
253
254 sub FirstAttribute {
255     my $self = shift;
256     my $name = shift;
257     return ($self->Attributes->Named( $name ))[0];
258 }
259
260
261 # {{{ sub _Handle 
262 sub _Handle {
263     my $self = shift;
264     return ($RT::Handle);
265 }
266
267 # }}}
268
269 # {{{ sub Create 
270
271 =head2  Create PARAMHASH
272
273 Takes a PARAMHASH of Column -> Value pairs.
274 If any Column has a Validate$PARAMNAME subroutine defined and the 
275 value provided doesn't pass validation, this routine returns
276 an error.
277
278 If this object's table has any of the following atetributes defined as
279 'Auto', this routine will automatically fill in their values.
280
281 =cut
282
283 sub Create {
284     my $self    = shift;
285     my %attribs = (@_);
286     foreach my $key ( keys %attribs ) {
287         my $method = "Validate$key";
288         unless ( $self->$method( $attribs{$key} ) ) {
289             if (wantarray) {
290                 return ( 0, $self->loc('Invalid value for [_1]', $key) );
291             }
292             else {
293                 return (0);
294             }
295         }
296     }
297     my $now = RT::Date->new( $self->CurrentUser );
298     $now->Set( Format => 'unix', Value => time );
299     $attribs{'Created'} = $now->ISO() if ( $self->_Accessible( 'Created', 'auto' ) && !$attribs{'Created'});
300
301     if ($self->_Accessible( 'Creator', 'auto' ) && !$attribs{'Creator'}) {
302          $attribs{'Creator'} = $self->CurrentUser->id || '0'; 
303     }
304     $attribs{'LastUpdated'} = $now->ISO()
305       if ( $self->_Accessible( 'LastUpdated', 'auto' ) && !$attribs{'LastUpdated'});
306
307     $attribs{'LastUpdatedBy'} = $self->CurrentUser->id || '0'
308       if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) && !$attribs{'LastUpdatedBy'});
309
310     my $id = $self->SUPER::Create(%attribs);
311     if ( UNIVERSAL::isa( $id, 'Class::ReturnValue' ) ) {
312         if ( $id->errno ) {
313             if (wantarray) {
314                 return ( 0,
315                     $self->loc( "Internal Error: [_1]", $id->{error_message} ) );
316             }
317             else {
318                 return (0);
319             }
320         }
321     }
322     # If the object was created in the database, 
323     # load it up now, so we're sure we get what the database 
324     # has.  Arguably, this should not be necessary, but there
325     # isn't much we can do about it.
326
327    unless ($id) { 
328     if (wantarray) {
329         return ( $id, $self->loc('Object could not be created') );
330     }
331     else {
332         return ($id);
333     }
334
335    }
336
337     if  (UNIVERSAL::isa('errno',$id)) {
338         exit(0);
339        warn "It's here!";
340         return(undef);
341     }
342
343     $self->Load($id) if ($id);
344
345
346
347     if (wantarray) {
348         return ( $id, $self->loc('Object created') );
349     }
350     else {
351         return ($id);
352     }
353
354 }
355
356 # }}}
357
358 # {{{ sub LoadByCols
359
360 =head2 LoadByCols
361
362 Override DBIx::SearchBuilder::LoadByCols to do case-insensitive loads if the 
363 DB is case sensitive
364
365 =cut
366
367 sub LoadByCols {
368     my $self = shift;
369     my %hash = (@_);
370
371     # We don't want to hang onto this
372     delete $self->{'attributes'};
373
374     # If this database is case sensitive we need to uncase objects for
375     # explicit loading
376     if ( $self->_Handle->CaseSensitive ) {
377         my %newhash;
378         foreach my $key ( keys %hash ) {
379
380             # If we've been passed an empty value, we can't do the lookup. 
381             # We don't need to explicitly downcase integers or an id.
382             if ( $key =~ '^id$'
383                 || !defined( $hash{$key} )
384                 || $hash{$key} =~ /^\d+$/
385                  )
386             {
387                 $newhash{$key} = $hash{$key};
388             }
389             else {
390                 my ($op, $val, $func);
391                 ($key, $op, $val, $func) = $self->_Handle->_MakeClauseCaseInsensitive($key, '=', $hash{$key});
392                 $newhash{$key}->{operator} = $op;
393                 $newhash{$key}->{value} = $val;
394                 $newhash{$key}->{function} = $func;
395             }
396         }
397
398         # We've clobbered everything we care about. bash the old hash
399         # and replace it with the new hash
400         %hash = %newhash;
401     }
402     $self->SUPER::LoadByCols(%hash);
403 }
404
405 # }}}
406
407 # {{{ Datehandling
408
409 # There is room for optimizations in most of those subs:
410
411 # {{{ LastUpdatedObj
412
413 sub LastUpdatedObj {
414     my $self = shift;
415     my $obj  = new RT::Date( $self->CurrentUser );
416
417     $obj->Set( Format => 'sql', Value => $self->LastUpdated );
418     return $obj;
419 }
420
421 # }}}
422
423 # {{{ CreatedObj
424
425 sub CreatedObj {
426     my $self = shift;
427     my $obj  = new RT::Date( $self->CurrentUser );
428
429     $obj->Set( Format => 'sql', Value => $self->Created );
430
431     return $obj;
432 }
433
434 # }}}
435
436 # {{{ AgeAsString
437 #
438 # TODO: This should be deprecated
439 #
440 sub AgeAsString {
441     my $self = shift;
442     return ( $self->CreatedObj->AgeAsString() );
443 }
444
445 # }}}
446
447 # {{{ LastUpdatedAsString
448
449 # TODO this should be deprecated
450
451 sub LastUpdatedAsString {
452     my $self = shift;
453     if ( $self->LastUpdated ) {
454         return ( $self->LastUpdatedObj->AsString() );
455
456     }
457     else {
458         return "never";
459     }
460 }
461
462 # }}}
463
464 # {{{ CreatedAsString
465 #
466 # TODO This should be deprecated 
467 #
468 sub CreatedAsString {
469     my $self = shift;
470     return ( $self->CreatedObj->AsString() );
471 }
472
473 # }}}
474
475 # {{{ LongSinceUpdateAsString
476 #
477 # TODO This should be deprecated
478 #
479 sub LongSinceUpdateAsString {
480     my $self = shift;
481     if ( $self->LastUpdated ) {
482
483         return ( $self->LastUpdatedObj->AgeAsString() );
484
485     }
486     else {
487         return "never";
488     }
489 }
490
491 # }}}
492
493 # }}} Datehandling
494
495 # {{{ sub _Set 
496 #
497 sub _Set {
498     my $self = shift;
499
500     my %args = (
501         Field => undef,
502         Value => undef,
503         IsSQL => undef,
504         @_
505     );
506
507     #if the user is trying to modify the record
508     # TODO: document _why_ this code is here
509
510     if ( ( !defined( $args{'Field'} ) ) || ( !defined( $args{'Value'} ) ) ) {
511         $args{'Value'} = 0;
512     }
513
514     my $old_val = $self->__Value($args{'Field'});
515      $self->_SetLastUpdated();
516     my $ret = $self->SUPER::_Set(
517         Field => $args{'Field'},
518         Value => $args{'Value'},
519         IsSQL => $args{'IsSQL'}
520     );
521         my ($status, $msg) =  $ret->as_array();
522
523         # @values has two values, a status code and a message.
524
525     # $ret is a Class::ReturnValue object. as such, in a boolean context, it's a bool
526     # we want to change the standard "success" message
527     if ($status) {
528         $msg =
529           $self->loc(
530             "[_1] changed from [_2] to [_3]",
531             $args{'Field'},
532             ( $old_val ? "'$old_val'" : $self->loc("(no value)") ),
533             '"' . $self->__Value( $args{'Field'}) . '"' 
534           );
535       } else {
536
537           $msg = $self->CurrentUser->loc_fuzzy($msg);
538     }
539     return wantarray ? ($status, $msg) : $ret;     
540
541 }
542
543 # }}}
544
545 # {{{ sub _SetLastUpdated
546
547 =head2 _SetLastUpdated
548
549 This routine updates the LastUpdated and LastUpdatedBy columns of the row in question
550 It takes no options. Arguably, this is a bug
551
552 =cut
553
554 sub _SetLastUpdated {
555     my $self = shift;
556     use RT::Date;
557     my $now = new RT::Date( $self->CurrentUser );
558     $now->SetToNow();
559
560     if ( $self->_Accessible( 'LastUpdated', 'auto' ) ) {
561         my ( $msg, $val ) = $self->__Set(
562             Field => 'LastUpdated',
563             Value => $now->ISO
564         );
565     }
566     if ( $self->_Accessible( 'LastUpdatedBy', 'auto' ) ) {
567         my ( $msg, $val ) = $self->__Set(
568             Field => 'LastUpdatedBy',
569             Value => $self->CurrentUser->id
570         );
571     }
572 }
573
574 # }}}
575
576 # {{{ sub CreatorObj 
577
578 =head2 CreatorObj
579
580 Returns an RT::User object with the RT account of the creator of this row
581
582 =cut
583
584 sub CreatorObj {
585     my $self = shift;
586     unless ( exists $self->{'CreatorObj'} ) {
587
588         $self->{'CreatorObj'} = RT::User->new( $self->CurrentUser );
589         $self->{'CreatorObj'}->Load( $self->Creator );
590     }
591     return ( $self->{'CreatorObj'} );
592 }
593
594 # }}}
595
596 # {{{ sub LastUpdatedByObj
597
598 =head2 LastUpdatedByObj
599
600   Returns an RT::User object of the last user to touch this object
601
602 =cut
603
604 sub LastUpdatedByObj {
605     my $self = shift;
606     unless ( exists $self->{LastUpdatedByObj} ) {
607         $self->{'LastUpdatedByObj'} = RT::User->new( $self->CurrentUser );
608         $self->{'LastUpdatedByObj'}->Load( $self->LastUpdatedBy );
609     }
610     return $self->{'LastUpdatedByObj'};
611 }
612
613 # }}}
614
615 # {{{ sub URI 
616
617 =head2 URI
618
619 Returns this record's URI
620
621 =cut
622
623 sub URI {
624     my $self = shift;
625     my $uri = RT::URI::fsck_com_rt->new($self->CurrentUser);
626     return($uri->URIForObject($self));
627 }
628
629 # }}}
630
631 =head2 ValidateName NAME
632
633 Validate the name of the record we're creating. Mostly, just make sure it's not a numeric ID, which is invalid for Name
634
635 =cut
636
637 sub ValidateName {
638     my $self = shift;
639     my $value = shift;
640     if ($value && $value=~ /^\d+$/) {
641         return(0);
642     } else  {
643          return (1);
644     }
645 }
646
647
648
649 =head2 SQLType attribute
650
651 return the SQL type for the attribute 'attribute' as stored in _ClassAccessible
652
653 =cut
654
655 sub SQLType {
656     my $self = shift;
657     my $field = shift;
658
659     return ($self->_Accessible($field, 'type'));
660
661
662 }
663
664
665 sub __Value {
666     my $self  = shift;
667     my $field = shift;
668     my %args = ( decode_utf8 => 1,
669                  @_ );
670
671     unless (defined $field && $field) {
672         $RT::Logger->error("$self __Value called with undef field");
673     }
674     my $value = $self->SUPER::__Value($field);
675
676     return('') if ( !defined($value) || $value eq '');
677
678     if( $args{'decode_utf8'} ) {
679         # XXX: is_utf8 check should be here unless Encode bug would be fixed
680         # see http://rt.cpan.org/NoAuth/Bug.html?id=14559 
681         return Encode::decode_utf8($value) unless Encode::is_utf8($value);
682     } else {
683         # check is_utf8 here just to be shure
684         return Encode::encode_utf8($value) if Encode::is_utf8($value);
685     }
686     return $value;
687 }
688
689 # Set up defaults for DBIx::SearchBuilder::Record::Cachable
690
691 sub _CacheConfig {
692   {
693      'cache_p'        => 1,
694      'cache_for_sec'  => 30,
695   }
696 }
697
698
699
700 sub _BuildTableAttributes {
701     my $self = shift;
702
703     my $attributes;
704     if ( UNIVERSAL::can( $self, '_CoreAccessible' ) ) {
705        $attributes = $self->_CoreAccessible();
706     } elsif ( UNIVERSAL::can( $self, '_ClassAccessible' ) ) {
707        $attributes = $self->_ClassAccessible();
708
709     }
710
711     foreach my $column (%$attributes) {
712         foreach my $attr ( %{ $attributes->{$column} } ) {
713             $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
714         }
715     }
716     if ( UNIVERSAL::can( $self, '_OverlayAccessible' ) ) {
717         $attributes = $self->_OverlayAccessible();
718
719         foreach my $column (%$attributes) {
720             foreach my $attr ( %{ $attributes->{$column} } ) {
721                 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
722             }
723         }
724     }
725     if ( UNIVERSAL::can( $self, '_VendorAccessible' ) ) {
726         $attributes = $self->_VendorAccessible();
727
728         foreach my $column (%$attributes) {
729             foreach my $attr ( %{ $attributes->{$column} } ) {
730                 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
731             }
732         }
733     }
734     if ( UNIVERSAL::can( $self, '_LocalAccessible' ) ) {
735         $attributes = $self->_LocalAccessible();
736
737         foreach my $column (%$attributes) {
738             foreach my $attr ( %{ $attributes->{$column} } ) {
739                 $_TABLE_ATTR->{ref($self)}->{$column}->{$attr} = $attributes->{$column}->{$attr};
740             }
741         }
742     }
743
744 }
745
746
747 =head2 _ClassAccessible 
748
749 Overrides the "core" _ClassAccessible using $_TABLE_ATTR. Behaves identical to the version in
750 DBIx::SearchBuilder::Record
751
752 =cut
753
754 sub _ClassAccessible {
755     my $self = shift;
756     return $_TABLE_ATTR->{ref($self)};
757 }
758
759 =head2 _Accessible COLUMN ATTRIBUTE
760
761 returns the value of ATTRIBUTE for COLUMN
762
763
764 =cut 
765
766 sub _Accessible  {
767   my $self = shift;
768   my $column = shift;
769   my $attribute = lc(shift);
770   return 0 unless defined ($_TABLE_ATTR->{ref($self)}->{$column});
771   return $_TABLE_ATTR->{ref($self)}->{$column}->{$attribute} || 0;
772
773 }
774
775 =head2 _EncodeLOB BODY MIME_TYPE
776
777 Takes a potentially large attachment. Returns (ContentEncoding, EncodedBody) based on system configuration and selected database
778
779 =cut
780
781 sub _EncodeLOB {
782         my $self = shift;
783         my $Body = shift;
784         my $MIMEType = shift;
785
786         my $ContentEncoding = 'none';
787
788         #get the max attachment length from RT
789         my $MaxSize = $RT::MaxAttachmentSize;
790
791         #if the current attachment contains nulls and the
792         #database doesn't support embedded nulls
793
794         if ( $RT::AlwaysUseBase64 or
795              ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) {
796
797             # set a flag telling us to mimencode the attachment
798             $ContentEncoding = 'base64';
799
800             #cut the max attchment size by 25% (for mime-encoding overhead.
801             $RT::Logger->debug("Max size is $MaxSize\n");
802             $MaxSize = $MaxSize * 3 / 4;
803         # Some databases (postgres) can't handle non-utf8 data
804         } elsif (    !$RT::Handle->BinarySafeBLOBs
805                   && $MIMEType !~ /text\/plain/gi
806                   && !Encode::is_utf8( $Body, 1 ) ) {
807               $ContentEncoding = 'quoted-printable';
808         }
809
810         #if the attachment is larger than the maximum size
811         if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) {
812
813             # if we're supposed to truncate large attachments
814             if ($RT::TruncateLongAttachments) {
815
816                 # truncate the attachment to that length.
817                 $Body = substr( $Body, 0, $MaxSize );
818
819             }
820
821             # elsif we're supposed to drop large attachments on the floor,
822             elsif ($RT::DropLongAttachments) {
823
824                 # drop the attachment on the floor
825                 $RT::Logger->info( "$self: Dropped an attachment of size "
826                                    . length($Body) . "\n"
827                                    . "It started: " . substr( $Body, 0, 60 ) . "\n"
828                                  );
829                 return ("none", "Large attachment dropped" );
830             }
831         }
832
833         # if we need to mimencode the attachment
834         if ( $ContentEncoding eq 'base64' ) {
835
836             # base64 encode the attachment
837             Encode::_utf8_off($Body);
838             $Body = MIME::Base64::encode_base64($Body);
839
840         } elsif ($ContentEncoding eq 'quoted-printable') {
841             Encode::_utf8_off($Body);
842             $Body = MIME::QuotedPrint::encode($Body);
843         }
844
845
846         return ($ContentEncoding, $Body);
847
848 }
849
850 sub _DecodeLOB {
851     my $self            = shift;
852     my $ContentType     = shift;
853     my $ContentEncoding = shift;
854     my $Content         = shift;
855
856     if ( $ContentEncoding eq 'base64' ) {
857         $Content = MIME::Base64::decode_base64($Content);
858     }
859     elsif ( $ContentEncoding eq 'quoted-printable' ) {
860         $Content = MIME::QuotedPrint::decode($Content);
861     }
862     elsif ( $ContentEncoding && $ContentEncoding ne 'none' ) {
863         return ( $self->loc( "Unknown ContentEncoding [_1]", $ContentEncoding ) );
864     }
865     if ( RT::I18N::IsTextualContentType($ContentType) ) {
866        $Content = Encode::decode_utf8($Content) unless Encode::is_utf8($Content);
867     }
868         return ($Content);
869 }
870
871 # {{{ LINKDIRMAP
872 # A helper table for links mapping to make it easier
873 # to build and parse links between tickets
874
875 use vars '%LINKDIRMAP';
876
877 %LINKDIRMAP = (
878     MemberOf => { Base => 'MemberOf',
879                   Target => 'HasMember', },
880     RefersTo => { Base => 'RefersTo',
881                 Target => 'ReferredToBy', },
882     DependsOn => { Base => 'DependsOn',
883                    Target => 'DependedOnBy', },
884     MergedInto => { Base => 'MergedInto',
885                    Target => 'MergedInto', },
886
887 );
888
889 =head2 Update  ARGSHASH
890
891 Updates fields on an object for you using the proper Set methods,
892 skipping unchanged values.
893
894  ARGSRef => a hashref of attributes => value for the update
895  AttributesRef => an arrayref of keys in ARGSRef that should be updated
896  AttributePrefix => a prefix that should be added to the attributes in AttributesRef
897                     when looking up values in ARGSRef
898                     Bare attributes are tried before prefixed attributes
899
900 Returns a list of localized results of the update
901
902 =cut
903
904 sub Update {
905     my $self = shift;
906
907     my %args = (
908         ARGSRef         => undef,
909         AttributesRef   => undef,
910         AttributePrefix => undef,
911         @_
912     );
913
914     my $attributes = $args{'AttributesRef'};
915     my $ARGSRef    = $args{'ARGSRef'};
916     my @results;
917
918     foreach my $attribute (@$attributes) {
919         my $value;
920         if ( defined $ARGSRef->{$attribute} ) {
921             $value = $ARGSRef->{$attribute};
922         }
923         elsif (
924             defined( $args{'AttributePrefix'} )
925             && defined(
926                 $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }
927             )
928           ) {
929             $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute };
930
931         }
932         else {
933             next;
934         }
935
936         $value =~ s/\r\n/\n/gs;
937
938
939         # If Queue is 'General', we want to resolve the queue name for
940         # the object.
941
942         # This is in an eval block because $object might not exist.
943         # and might not have a Name method. But "can" won't find autoloaded
944         # items. If it fails, we don't care
945         eval {
946             my $object = $attribute . "Obj";
947             next if ($self->$object->Name eq $value);
948         };
949         next if ( $value eq $self->$attribute() );
950         my $method = "Set$attribute";
951         my ( $code, $msg ) = $self->$method($value);
952         my ($prefix) = ref($self) =~ /RT(?:.*)::(\w+)/;
953
954         # Default to $id, but use name if we can get it.
955         my $label = $self->id;
956         $label = $self->Name if (UNIVERSAL::can($self,'Name'));
957         push @results, $self->loc( "$prefix [_1]", $label ) . ': '. $msg;
958
959 =for loc
960
961                                    "[_1] could not be set to [_2].",       # loc
962                                    "That is already the current value",    # loc
963                                    "No value sent to _Set!\n",             # loc
964                                    "Illegal value for [_1]",               # loc
965                                    "The new value has been set.",          # loc
966                                    "No column specified",                  # loc
967                                    "Immutable field",                      # loc
968                                    "Nonexistant field?",                   # loc
969                                    "Invalid data",                         # loc
970                                    "Couldn't find row",                    # loc
971                                    "Missing a primary key?: [_1]",         # loc
972                                    "Found Object",                         # loc
973
974 =cut
975
976     }
977
978     return @results;
979 }
980
981 # {{{ Routines dealing with Links
982
983 # {{{ Link Collections
984
985 # {{{ sub Members
986
987 =head2 Members
988
989   This returns an RT::Links object which references all the tickets 
990 which are 'MembersOf' this ticket
991
992 =cut
993
994 sub Members {
995     my $self = shift;
996     return ( $self->_Links( 'Target', 'MemberOf' ) );
997 }
998
999 # }}}
1000
1001 # {{{ sub MemberOf
1002
1003 =head2 MemberOf
1004
1005   This returns an RT::Links object which references all the tickets that this
1006 ticket is a 'MemberOf'
1007
1008 =cut
1009
1010 sub MemberOf {
1011     my $self = shift;
1012     return ( $self->_Links( 'Base', 'MemberOf' ) );
1013 }
1014
1015 # }}}
1016
1017 # {{{ RefersTo
1018
1019 =head2 RefersTo
1020
1021   This returns an RT::Links object which shows all references for which this ticket is a base
1022
1023 =cut
1024
1025 sub RefersTo {
1026     my $self = shift;
1027     return ( $self->_Links( 'Base', 'RefersTo' ) );
1028 }
1029
1030 # }}}
1031
1032 # {{{ ReferredToBy
1033
1034 =head2 ReferredToBy
1035
1036 This returns an L<RT::Links> object which shows all references for which this ticket is a target
1037
1038 =cut
1039
1040 sub ReferredToBy {
1041     my $self = shift;
1042     return ( $self->_Links( 'Target', 'RefersTo' ) );
1043 }
1044
1045 # }}}
1046
1047 # {{{ DependedOnBy
1048
1049 =head2 DependedOnBy
1050
1051   This returns an RT::Links object which references all the tickets that depend on this one
1052
1053 =cut
1054
1055 sub DependedOnBy {
1056     my $self = shift;
1057     return ( $self->_Links( 'Target', 'DependsOn' ) );
1058 }
1059
1060 # }}}
1061
1062
1063
1064 =head2 HasUnresolvedDependencies
1065
1066   Takes a paramhash of Type (default to '__any').  Returns true if
1067 $self->UnresolvedDependencies returns an object with one or more members
1068 of that type.  Returns false otherwise
1069
1070
1071 =begin testing
1072
1073 my $t1 = RT::Ticket->new($RT::SystemUser);
1074 my ($id, $trans, $msg) = $t1->Create(Subject => 'DepTest1', Queue => 'general');
1075 ok($id, "Created dep test 1 - $msg");
1076
1077 my $t2 = RT::Ticket->new($RT::SystemUser);
1078 my ($id2, $trans, $msg2) = $t2->Create(Subject => 'DepTest2', Queue => 'general');
1079 ok($id2, "Created dep test 2 - $msg2");
1080 my $t3 = RT::Ticket->new($RT::SystemUser);
1081 my ($id3, $trans, $msg3) = $t3->Create(Subject => 'DepTest3', Queue => 'general', Type => 'approval');
1082 ok($id3, "Created dep test 3 - $msg3");
1083 my ($addid, $addmsg);
1084 ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t2->id));
1085 ok ($addid, $addmsg);
1086 ok (($addid, $addmsg) =$t1->AddLink( Type => 'DependsOn', Target => $t3->id));
1087
1088 ok ($addid, $addmsg);
1089 my $link = RT::Link->new($RT::SystemUser);
1090 my ($rv, $msg) = $link->Load($addid);
1091 ok ($rv, $msg);
1092 ok ($link->LocalTarget == $t3->id, "Link LocalTarget is correct");
1093 ok ($link->LocalBase   == $t1->id, "Link LocalBase   is correct");
1094
1095 ok ($t1->HasUnresolvedDependencies, "Ticket ".$t1->Id." has unresolved deps");
1096 ok (!$t1->HasUnresolvedDependencies( Type => 'blah' ), "Ticket ".$t1->Id." has no unresolved blahs");
1097 ok ($t1->HasUnresolvedDependencies( Type => 'approval' ), "Ticket ".$t1->Id." has unresolved approvals");
1098 ok (!$t2->HasUnresolvedDependencies, "Ticket ".$t2->Id." has no unresolved deps");
1099 ;
1100
1101 my ($rid, $rmsg)= $t1->Resolve();
1102 ok(!$rid, $rmsg);
1103 my ($rid2, $rmsg2) = $t2->Resolve();
1104 ok ($rid2, $rmsg2);
1105 ($rid, $rmsg)= $t1->Resolve();
1106 ok(!$rid, $rmsg);
1107 my ($rid3,$rmsg3) = $t3->Resolve;
1108 ok ($rid3,$rmsg3);
1109 ($rid, $rmsg)= $t1->Resolve();
1110 ok($rid, $rmsg);
1111
1112
1113 =end testing
1114
1115 =cut
1116
1117 sub HasUnresolvedDependencies {
1118     my $self = shift;
1119     my %args = (
1120         Type   => undef,
1121         @_
1122     );
1123
1124     my $deps = $self->UnresolvedDependencies;
1125
1126     if ($args{Type}) {
1127         $deps->Limit( FIELD => 'Type', 
1128               OPERATOR => '=',
1129               VALUE => $args{Type}); 
1130     }
1131     else {
1132             $deps->IgnoreType;
1133     }
1134
1135     if ($deps->Count > 0) {
1136         return 1;
1137     }
1138     else {
1139         return (undef);
1140     }
1141 }
1142
1143
1144 # {{{ UnresolvedDependencies 
1145
1146 =head2 UnresolvedDependencies
1147
1148 Returns an RT::Tickets object of tickets which this ticket depends on
1149 and which have a status of new, open or stalled. (That list comes from
1150 RT::Queue->ActiveStatusArray
1151
1152 =cut
1153
1154
1155 sub UnresolvedDependencies {
1156     my $self = shift;
1157     my $deps = RT::Tickets->new($self->CurrentUser);
1158
1159     my @live_statuses = RT::Queue->ActiveStatusArray();
1160     foreach my $status (@live_statuses) {
1161         $deps->LimitStatus(VALUE => $status);
1162     }
1163     $deps->LimitDependedOnBy($self->Id);
1164
1165     return($deps);
1166
1167 }
1168
1169 # }}}
1170
1171 # {{{ AllDependedOnBy
1172
1173 =head2 AllDependedOnBy
1174
1175 Returns an array of RT::Ticket objects which (directly or indirectly)
1176 depends on this ticket; takes an optional 'Type' argument in the param
1177 hash, which will limit returned tickets to that type, as well as cause
1178 tickets with that type to serve as 'leaf' nodes that stops the recursive
1179 dependency search.
1180
1181 =cut
1182
1183 sub AllDependedOnBy {
1184     my $self = shift;
1185     my $dep = $self->DependedOnBy;
1186     my %args = (
1187         Type   => undef,
1188         _found => {},
1189         _top   => 1,
1190         @_
1191     );
1192
1193     while (my $link = $dep->Next()) {
1194         next unless ($link->BaseURI->IsLocal());
1195         next if $args{_found}{$link->BaseObj->Id};
1196
1197         if (!$args{Type}) {
1198             $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
1199             $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
1200         }
1201         elsif ($link->BaseObj->Type eq $args{Type}) {
1202             $args{_found}{$link->BaseObj->Id} = $link->BaseObj;
1203         }
1204         else {
1205             $link->BaseObj->AllDependedOnBy( %args, _top => 0 );
1206         }
1207     }
1208
1209     if ($args{_top}) {
1210         return map { $args{_found}{$_} } sort keys %{$args{_found}};
1211     }
1212     else {
1213         return 1;
1214     }
1215 }
1216
1217 # }}}
1218
1219 # {{{ DependsOn
1220
1221 =head2 DependsOn
1222
1223   This returns an RT::Links object which references all the tickets that this ticket depends on
1224
1225 =cut
1226
1227 sub DependsOn {
1228     my $self = shift;
1229     return ( $self->_Links( 'Base', 'DependsOn' ) );
1230 }
1231
1232 # }}}
1233
1234
1235
1236
1237 # {{{ sub _Links 
1238
1239 =head2 Links DIRECTION [TYPE]
1240
1241 Return links (L<RT::Links>) to/from this object.
1242
1243 DIRECTION is either 'Base' or 'Target'.
1244
1245 TYPE is a type of links to return, it can be omitted to get
1246 links of any type.
1247
1248 =cut
1249
1250 *Links = \&_Links;
1251
1252 sub _Links {
1253     my $self = shift;
1254
1255     #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1256     #tobias meant by $f
1257     my $field = shift;
1258     my $type  = shift || "";
1259
1260     unless ( $self->{"$field$type"} ) {
1261         $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
1262             # at least to myself
1263             $self->{"$field$type"}->Limit( FIELD => $field,
1264                                            VALUE => $self->URI,
1265                                            ENTRYAGGREGATOR => 'OR' );
1266             $self->{"$field$type"}->Limit( FIELD => 'Type',
1267                                            VALUE => $type )
1268               if ($type);
1269     }
1270     return ( $self->{"$field$type"} );
1271 }
1272
1273 # }}}
1274
1275 # }}}
1276
1277 # {{{ sub _AddLink
1278
1279 =head2 _AddLink
1280
1281 Takes a paramhash of Type and one of Base or Target. Adds that link to this object.
1282
1283 Returns C<link id>, C<message> and C<exist> flag.
1284
1285
1286 =cut
1287
1288
1289 sub _AddLink {
1290     my $self = shift;
1291     my %args = ( Target => '',
1292                  Base   => '',
1293                  Type   => '',
1294                  Silent => undef,
1295                  @_ );
1296
1297
1298     # Remote_link is the URI of the object that is not this ticket
1299     my $remote_link;
1300     my $direction;
1301
1302     if ( $args{'Base'} and $args{'Target'} ) {
1303         $RT::Logger->debug( "$self tried to create a link. both base and target were specified\n" );
1304         return ( 0, $self->loc("Can't specifiy both base and target") );
1305     }
1306     elsif ( $args{'Base'} ) {
1307         $args{'Target'} = $self->URI();
1308         $remote_link    = $args{'Base'};
1309         $direction      = 'Target';
1310     }
1311     elsif ( $args{'Target'} ) {
1312         $args{'Base'} = $self->URI();
1313         $remote_link  = $args{'Target'};
1314         $direction    = 'Base';
1315     }
1316     else {
1317         return ( 0, $self->loc('Either base or target must be specified') );
1318     }
1319
1320     # {{{ Check if the link already exists - we don't want duplicates
1321     use RT::Link;
1322     my $old_link = RT::Link->new( $self->CurrentUser );
1323     $old_link->LoadByParams( Base   => $args{'Base'},
1324                              Type   => $args{'Type'},
1325                              Target => $args{'Target'} );
1326     if ( $old_link->Id ) {
1327         $RT::Logger->debug("$self Somebody tried to duplicate a link");
1328         return ( $old_link->id, $self->loc("Link already exists"), 1 );
1329     }
1330
1331     # }}}
1332
1333
1334     # Storing the link in the DB.
1335     my $link = RT::Link->new( $self->CurrentUser );
1336     my ($linkid, $linkmsg) = $link->Create( Target => $args{Target},
1337                                   Base   => $args{Base},
1338                                   Type   => $args{Type} );
1339
1340     unless ($linkid) {
1341         $RT::Logger->error("Link could not be created: ".$linkmsg);
1342         return ( 0, $self->loc("Link could not be created") );
1343     }
1344
1345     my $TransString =
1346       "Record $args{'Base'} $args{Type} record $args{'Target'}.";
1347
1348     return ( $linkid, $self->loc( "Link created ([_1])", $TransString ) );
1349 }
1350
1351 # }}}
1352
1353 # {{{ sub _DeleteLink 
1354
1355 =head2 _DeleteLink
1356
1357 Delete a link. takes a paramhash of Base, Target and Type.
1358 Either Base or Target must be null. The null value will 
1359 be replaced with this ticket\'s id
1360
1361 =cut 
1362
1363 sub _DeleteLink {
1364     my $self = shift;
1365     my %args = (
1366         Base   => undef,
1367         Target => undef,
1368         Type   => undef,
1369         @_
1370     );
1371
1372     #we want one of base and target. we don't care which
1373     #but we only want _one_
1374
1375     my $direction;
1376     my $remote_link;
1377
1378     if ( $args{'Base'} and $args{'Target'} ) {
1379         $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
1380         return ( 0, $self->loc("Can't specifiy both base and target") );
1381     }
1382     elsif ( $args{'Base'} ) {
1383         $args{'Target'} = $self->URI();
1384         $remote_link = $args{'Base'};
1385         $direction = 'Target';
1386     }
1387     elsif ( $args{'Target'} ) {
1388         $args{'Base'} = $self->URI();
1389         $remote_link = $args{'Target'};
1390         $direction='Base';
1391     }
1392     else {
1393         $RT::Logger->error("Base or Target must be specified\n");
1394         return ( 0, $self->loc('Either base or target must be specified') );
1395     }
1396
1397     my $link = new RT::Link( $self->CurrentUser );
1398     $RT::Logger->debug( "Trying to load link: " . $args{'Base'} . " " . $args{'Type'} . " " . $args{'Target'} . "\n" );
1399
1400
1401     $link->LoadByParams( Base=> $args{'Base'}, Type=> $args{'Type'}, Target=>  $args{'Target'} );
1402     #it's a real link. 
1403     if ( $link->id ) {
1404
1405         my $linkid = $link->id;
1406         $link->Delete();
1407
1408         my $TransString = "Record $args{'Base'} no longer $args{Type} record $args{'Target'}.";
1409         return ( 1, $self->loc("Link deleted ([_1])", $TransString));
1410     }
1411
1412     #if it's not a link we can find
1413     else {
1414         $RT::Logger->debug("Couldn't find that link\n");
1415         return ( 0, $self->loc("Link not found") );
1416     }
1417 }
1418
1419 # }}}
1420
1421 # }}}
1422
1423 # {{{ Routines dealing with transactions
1424
1425 # {{{ sub _NewTransaction
1426
1427 =head2 _NewTransaction  PARAMHASH
1428
1429 Private function to create a new RT::Transaction object for this ticket update
1430
1431 =cut
1432
1433 sub _NewTransaction {
1434     my $self = shift;
1435     my %args = (
1436         TimeTaken => undef,
1437         Type      => undef,
1438         OldValue  => undef,
1439         NewValue  => undef,
1440         OldReference  => undef,
1441         NewReference  => undef,
1442         ReferenceType => undef,
1443         Data      => undef,
1444         Field     => undef,
1445         MIMEObj   => undef,
1446         ActivateScrips => 1,
1447         CommitScrips => 1,
1448         @_
1449     );
1450
1451     my $old_ref = $args{'OldReference'};
1452     my $new_ref = $args{'NewReference'};
1453     my $ref_type = $args{'ReferenceType'};
1454     if ($old_ref or $new_ref) {
1455         $ref_type ||= ref($old_ref) || ref($new_ref);
1456         if (!$ref_type) {
1457             $RT::Logger->error("Reference type not specified for transaction");
1458             return;
1459         }
1460         $old_ref = $old_ref->Id if ref($old_ref);
1461         $new_ref = $new_ref->Id if ref($new_ref);
1462     }
1463
1464     require RT::Transaction;
1465     my $trans = new RT::Transaction( $self->CurrentUser );
1466     my ( $transaction, $msg ) = $trans->Create(
1467         ObjectId  => $self->Id,
1468         ObjectType => ref($self),
1469         TimeTaken => $args{'TimeTaken'},
1470         Type      => $args{'Type'},
1471         Data      => $args{'Data'},
1472         Field     => $args{'Field'},
1473         NewValue  => $args{'NewValue'},
1474         OldValue  => $args{'OldValue'},
1475         NewReference  => $new_ref,
1476         OldReference  => $old_ref,
1477         ReferenceType => $ref_type,
1478         MIMEObj   => $args{'MIMEObj'},
1479         ActivateScrips => $args{'ActivateScrips'},
1480         CommitScrips => $args{'CommitScrips'},
1481     );
1482
1483     # Rationalize the object since we may have done things to it during the caching.
1484     $self->Load($self->Id);
1485
1486     $RT::Logger->warning($msg) unless $transaction;
1487
1488     $self->_SetLastUpdated;
1489
1490     if ( defined $args{'TimeTaken'} and $self->can('_UpdateTimeTaken')) {
1491         $self->_UpdateTimeTaken( $args{'TimeTaken'} );
1492     }
1493     if ( $RT::UseTransactionBatch and $transaction ) {
1494             push @{$self->{_TransactionBatch}}, $trans if $args{'CommitScrips'};
1495     }
1496     return ( $transaction, $msg, $trans );
1497 }
1498
1499 # }}}
1500
1501 # {{{ sub Transactions 
1502
1503 =head2 Transactions
1504
1505   Returns an RT::Transactions object of all transactions on this record object
1506
1507 =cut
1508
1509 sub Transactions {
1510     my $self = shift;
1511
1512     use RT::Transactions;
1513     my $transactions = RT::Transactions->new( $self->CurrentUser );
1514
1515     #If the user has no rights, return an empty object
1516     $transactions->Limit(
1517         FIELD => 'ObjectId',
1518         VALUE => $self->id,
1519     );
1520     $transactions->Limit(
1521         FIELD => 'ObjectType',
1522         VALUE => ref($self),
1523     );
1524
1525     return ($transactions);
1526 }
1527
1528 # }}}
1529 # }}}
1530 #
1531 # {{{ Routines dealing with custom fields
1532
1533 sub CustomFields {
1534     my $self = shift;
1535     my $cfs  = RT::CustomFields->new( $self->CurrentUser );
1536
1537     # XXX handle multiple types properly
1538     $cfs->LimitToLookupType( $self->CustomFieldLookupType );
1539     $cfs->LimitToGlobalOrObjectId(
1540         $self->_LookupId( $self->CustomFieldLookupType ) );
1541
1542     return $cfs;
1543 }
1544
1545 # TODO: This _only_ works for RT::Class classes. it doesn't work, for example, for RT::FM classes.
1546
1547 sub _LookupId {
1548     my $self = shift;
1549     my $lookup = shift;
1550     my @classes = ($lookup =~ /RT::(\w+)-/g);
1551
1552     my $object = $self;
1553     foreach my $class (reverse @classes) {
1554         my $method = "${class}Obj";
1555         $object = $object->$method;
1556     }
1557
1558     return $object->Id;
1559 }
1560
1561
1562 =head2 CustomFieldLookupType 
1563
1564 Returns the path RT uses to figure out which custom fields apply to this object.
1565
1566 =cut
1567
1568 sub CustomFieldLookupType {
1569     my $self = shift;
1570     return ref($self);
1571 }
1572
1573 #TODO Deprecated API. Destroy in 3.6
1574 sub _LookupTypes { 
1575     my  $self = shift;
1576     $RT::Logger->warning("_LookupTypes call is deprecated at (". join(":",caller)."). Replace with CustomFieldLookupType");
1577
1578     return($self->CustomFieldLookupType);
1579
1580 }
1581
1582 # {{{ AddCustomFieldValue
1583
1584 =head2 AddCustomFieldValue { Field => FIELD, Value => VALUE }
1585
1586 VALUE should be a string.
1587 FIELD can be a CustomField object OR a CustomField ID.
1588
1589
1590 Adds VALUE as a value of CustomField FIELD.  If this is a single-value custom field,
1591 deletes the old value. 
1592 If VALUE is not a valid value for the custom field, returns 
1593 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
1594
1595 =cut
1596
1597 sub AddCustomFieldValue {
1598     my $self = shift;
1599     $self->_AddCustomFieldValue(@_);
1600 }
1601
1602 sub _AddCustomFieldValue {
1603     my $self = shift;
1604     my %args = (
1605         Field             => undef,
1606         Value             => undef,
1607         RecordTransaction => 1,
1608         @_
1609     );
1610
1611     my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1612
1613     unless ( $cf->Id ) {
1614         return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1615     }
1616
1617     my $OCFs = $self->CustomFields;
1618     $OCFs->Limit( FIELD => 'id', VALUE => $cf->Id );
1619     unless ( $OCFs->Count ) {
1620         return (
1621             0,
1622             $self->loc(
1623                 "Custom field [_1] does not apply to this object",
1624                 $args{'Field'}
1625             )
1626         );
1627     }
1628     # Load up a ObjectCustomFieldValues object for this custom field and this ticket
1629     my $values = $cf->ValuesForObject($self);
1630
1631     unless ( $cf->ValidateValue( $args{'Value'} ) ) {
1632         return ( 0, $self->loc("Invalid value for custom field") );
1633     }
1634
1635     # If the custom field only accepts a certain # of values, delete the existing
1636     # value and record a "changed from foo to bar" transaction
1637     unless ( $cf->UnlimitedValues) {
1638
1639  # We need to whack any old values here.  In most cases, the custom field should
1640  # only have one value to delete.  In the pathalogical case, this custom field
1641  # used to be a multiple and we have many values to whack....
1642         my $cf_values = $values->Count;
1643
1644         if ( $cf_values > $cf->MaxValues ) {
1645             my $i = 0;   #We want to delete all but the max we can currently have , so we can then
1646                  # execute the same code to "change" the value from old to new
1647             while ( my $value = $values->Next ) {
1648                 $i++;
1649                 if ( $i < $cf_values ) {
1650                     my ( $val, $msg ) = $cf->DeleteValueForObject(
1651                         Object  => $self,
1652                         Content => $value->Content
1653                     );
1654                     unless ($val) {
1655                         return ( 0, $msg );
1656                     }
1657                     my ( $TransactionId, $Msg, $TransactionObj ) =
1658                       $self->_NewTransaction(
1659                         Type         => 'CustomField',
1660                         Field        => $cf->Id,
1661                         OldReference => $value,
1662                       );
1663                 }
1664             }
1665             $values->RedoSearch if $i; # redo search if have deleted at least one value
1666         }
1667
1668         my ( $old_value, $old_content );
1669         if ( $old_value = $values->First ) {
1670             $old_content = $old_value->Content();
1671             return (1) if( $old_content eq $args{'Value'} && $old_value->LargeContent eq $args{'LargeContent'});;
1672         }
1673
1674         my ( $new_value_id, $value_msg ) = $cf->AddValueForObject(
1675             Object       => $self,
1676             Content      => $args{'Value'},
1677             LargeContent => $args{'LargeContent'},
1678             ContentType  => $args{'ContentType'},
1679         );
1680
1681         unless ($new_value_id) {
1682             return ( 0, $self->loc( "Could not add new custom field value: [_1]", $value_msg) );
1683         }
1684
1685         my $new_value = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1686         $new_value->Load($new_value_id);
1687
1688         # now that adding the new value was successful, delete the old one
1689         if ($old_value) {
1690             my ( $val, $msg ) = $old_value->Delete();
1691             unless ($val) {
1692                 return ( 0, $msg );
1693             }
1694         }
1695
1696         if ( $args{'RecordTransaction'} ) {
1697             my ( $TransactionId, $Msg, $TransactionObj ) =
1698               $self->_NewTransaction(
1699                 Type         => 'CustomField',
1700                 Field        => $cf->Id,
1701                 OldReference => $old_value,
1702                 NewReference => $new_value,
1703               );
1704         }
1705
1706         if ( $old_value eq '' ) {
1707             return ( 1, $self->loc( "[_1] [_2] added", $cf->Name, $new_value->Content ));
1708         }
1709         elsif ( $new_value->Content eq '' ) {
1710             return ( 1,
1711                 $self->loc( "[_1] [_2] deleted", $cf->Name, $old_value->Content ) );
1712         }
1713         else {
1714             return ( 1, $self->loc( "[_1] [_2] changed to [_3]", $cf->Name, $old_content,                $new_value->Content));
1715         }
1716
1717     }
1718
1719     # otherwise, just add a new value and record "new value added"
1720     else {
1721         my ($new_value_id, $value_msg) = $cf->AddValueForObject(
1722             Object       => $self,
1723             Content      => $args{'Value'},
1724             LargeContent => $args{'LargeContent'},
1725             ContentType  => $args{'ContentType'},
1726         );
1727
1728         unless ($new_value_id) {
1729             return ( 0, $self->loc( "Could not add new custom field value: [_1]", $value_msg) );
1730         }
1731         if ( $args{'RecordTransaction'} ) {
1732             my ( $TransactionId, $Msg, $TransactionObj ) =
1733               $self->_NewTransaction(
1734                 Type          => 'CustomField',
1735                 Field         => $cf->Id,
1736                 NewReference  => $new_value_id,
1737                 ReferenceType => 'RT::ObjectCustomFieldValue',
1738               );
1739             unless ($TransactionId) {
1740                 return ( 0,
1741                     $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
1742             }
1743         }
1744         return ( 1, $self->loc( "[_1] added as a value for [_2]", $args{'Value'}, $cf->Name));
1745     }
1746
1747 }
1748
1749 # }}}
1750
1751 # {{{ DeleteCustomFieldValue
1752
1753 =head2 DeleteCustomFieldValue { Field => FIELD, Value => VALUE }
1754
1755 Deletes VALUE as a value of CustomField FIELD. 
1756
1757 VALUE can be a string, a CustomFieldValue or a ObjectCustomFieldValue.
1758
1759 If VALUE is not a valid value for the custom field, returns 
1760 (0, 'Error message' ) otherwise, returns (1, 'Success Message')
1761
1762 =cut
1763
1764 sub DeleteCustomFieldValue {
1765     my $self = shift;
1766     my %args = (
1767         Field   => undef,
1768         Value   => undef,
1769         ValueId => undef,
1770         @_
1771     );
1772
1773     my $cf = $self->LoadCustomFieldByIdentifier($args{'Field'});
1774
1775     unless ( $cf->Id ) {
1776         return ( 0, $self->loc( "Custom field [_1] not found", $args{'Field'} ) );
1777     }
1778     my ( $val, $msg ) = $cf->DeleteValueForObject(
1779         Object  => $self,
1780         Id      => $args{'ValueId'},
1781         Content => $args{'Value'},
1782     );
1783     unless ($val) {
1784         return ( 0, $msg );
1785     }
1786     my ( $TransactionId, $Msg, $TransactionObj ) = $self->_NewTransaction(
1787         Type          => 'CustomField',
1788         Field         => $cf->Id,
1789         OldReference  => $val,
1790         ReferenceType => 'RT::ObjectCustomFieldValue',
1791     );
1792     unless ($TransactionId) {
1793         return ( 0, $self->loc( "Couldn't create a transaction: [_1]", $Msg ) );
1794     }
1795
1796     return (
1797         $TransactionId,
1798         $self->loc(
1799             "[_1] is no longer a value for custom field [_2]",
1800             $TransactionObj->OldValue, $cf->Name
1801         )
1802     );
1803 }
1804
1805 # }}}
1806
1807 # {{{ FirstCustomFieldValue
1808
1809 =head2 FirstCustomFieldValue FIELD
1810
1811 Return the content of the first value of CustomField FIELD for this ticket
1812 Takes a field id or name
1813
1814 =cut
1815
1816 sub FirstCustomFieldValue {
1817     my $self = shift;
1818     my $field = shift;
1819     my $values = $self->CustomFieldValues($field);
1820     if ($values->First) {
1821         return $values->First->Content;
1822     } else {
1823         return undef;
1824     }
1825
1826 }
1827
1828
1829
1830 # {{{ CustomFieldValues
1831
1832 =head2 CustomFieldValues FIELD
1833
1834 Return a ObjectCustomFieldValues object of all values of the CustomField whose 
1835 id or Name is FIELD for this record.
1836
1837 Returns an RT::ObjectCustomFieldValues object
1838
1839 =cut
1840
1841 sub CustomFieldValues {
1842     my $self  = shift;
1843     my $field = shift;
1844
1845     if ($field) {
1846         my $cf = $self->LoadCustomFieldByIdentifier($field);
1847
1848         # we were asked to search on a custom field we couldn't fine
1849         unless ( $cf->id ) {
1850             return RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1851         }
1852         return ( $cf->ValuesForObject($self) );
1853     }
1854
1855     # we're not limiting to a specific custom field;
1856     my $ocfs = RT::ObjectCustomFieldValues->new( $self->CurrentUser );
1857     $ocfs->LimitToObject($self);
1858     return $ocfs;
1859
1860 }
1861
1862 =head2 CustomField IDENTIFER
1863
1864 Find the custom field has id or name IDENTIFIER for this object.
1865
1866 If no valid field is found, returns an empty RT::CustomField object.
1867
1868 =cut
1869
1870 sub LoadCustomFieldByIdentifier {
1871     my $self = shift;
1872     my $field = shift;
1873     
1874     my $cf = RT::CustomField->new($self->CurrentUser);
1875
1876     if ( UNIVERSAL::isa( $field, "RT::CustomField" ) ) {
1877         $cf->LoadById( $field->id );
1878     }
1879     elsif ($field =~ /^\d+$/) {
1880         $cf = RT::CustomField->new($self->CurrentUser);
1881         $cf->Load($field); 
1882     } else {
1883
1884         my $cfs = $self->CustomFields($self->CurrentUser);
1885         $cfs->Limit(FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0);
1886         $cf = $cfs->First || RT::CustomField->new($self->CurrentUser);
1887     }
1888     return $cf;
1889 }
1890
1891
1892 # }}}
1893
1894 # }}}
1895
1896 # }}}
1897
1898 sub BasicColumns {
1899 }
1900
1901 sub WikiBase {
1902   return $RT::WebPath. "/index.html?q=";
1903 }
1904
1905 eval "require RT::Record_Vendor";
1906 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Record_Vendor.pm});
1907 eval "require RT::Record_Local";
1908 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Record_Local.pm});
1909
1910 1;