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