]> git.uio.no Git - usit-rt.git/blob - lib/RT/CustomField.pm
Merge branch 'master' of git.uio.no:usit-rt
[usit-rt.git] / lib / RT / CustomField.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::CustomField;
50
51 use strict;
52 use warnings;
53 use 5.010;
54
55 use Scalar::Util 'blessed';
56
57 use base 'RT::Record';
58
59 use Role::Basic 'with';
60 with "RT::Record::Role::Rights";
61
62 sub Table {'CustomFields'}
63
64 use Scalar::Util qw(blessed);
65 use RT::CustomFieldValues;
66 use RT::ObjectCustomFields;
67 use RT::ObjectCustomFieldValues;
68
69 our %FieldTypes = (
70     Select => {
71         sort_order => 10,
72         selection_type => 1,
73
74         labels => [ 'Select multiple values',               # loc
75                     'Select one value',                     # loc
76                     'Select up to [quant,_1,value,values]', # loc
77                   ],
78
79         render_types => {
80             multiple => [
81
82                 # Default is the first one
83                 'Select box',              # loc
84                 'List',                    # loc
85             ],
86             single => [ 'Select box',              # loc
87                         'Dropdown',                # loc
88                         'List',                    # loc
89                       ]
90         },
91
92     },
93     Freeform => {
94         sort_order => 20,
95         selection_type => 0,
96
97         labels => [ 'Enter multiple values',               # loc
98                     'Enter one value',                     # loc
99                     'Enter up to [quant,_1,value,values]', # loc
100                   ]
101                 },
102     Text => {
103         sort_order => 30,
104         selection_type => 0,
105         labels         => [
106                     'Fill in multiple text areas',                   # loc
107                     'Fill in one text area',                         # loc
108                     'Fill in up to [quant,_1,text area,text areas]', # loc
109                   ]
110             },
111     Wikitext => {
112         sort_order => 40,
113         selection_type => 0,
114         labels         => [
115                     'Fill in multiple wikitext areas',                       # loc
116                     'Fill in one wikitext area',                             # loc
117                     'Fill in up to [quant,_1,wikitext area,wikitext areas]', # loc
118                   ]
119                 },
120
121     Image => {
122         sort_order => 50,
123         selection_type => 0,
124         labels         => [
125                     'Upload multiple images',               # loc
126                     'Upload one image',                     # loc
127                     'Upload up to [quant,_1,image,images]', # loc
128                   ]
129              },
130     Binary => {
131         sort_order => 60,
132         selection_type => 0,
133         labels         => [
134                     'Upload multiple files',              # loc
135                     'Upload one file',                    # loc
136                     'Upload up to [quant,_1,file,files]', # loc
137                   ]
138               },
139
140     Combobox => {
141         sort_order => 70,
142         selection_type => 1,
143         labels         => [
144                     'Combobox: Select or enter multiple values',               # loc
145                     'Combobox: Select or enter one value',                     # loc
146                     'Combobox: Select or enter up to [quant,_1,value,values]', # loc
147                   ]
148                 },
149     Autocomplete => {
150         sort_order => 80,
151         selection_type => 1,
152         labels         => [
153                     'Enter multiple values with autocompletion',               # loc
154                     'Enter one value with autocompletion',                     # loc
155                     'Enter up to [quant,_1,value,values] with autocompletion', # loc
156                   ]
157     },
158
159     Date => {
160         sort_order => 90,
161         selection_type => 0,
162         labels         => [
163                     'Select multiple dates',              # loc
164                     'Select date',                        # loc
165                     'Select up to [quant,_1,date,dates]', # loc
166                   ]
167             },
168     DateTime => {
169         sort_order => 100,
170         selection_type => 0,
171         labels         => [
172                     'Select multiple datetimes',                  # loc
173                     'Select datetime',                            # loc
174                     'Select up to [quant,_1,datetime,datetimes]', # loc
175                   ]
176                 },
177
178     IPAddress => {
179         sort_order => 110,
180         selection_type => 0,
181
182         labels => [ 'Enter multiple IP addresses',                    # loc
183                     'Enter one IP address',                           # loc
184                     'Enter up to [quant,_1,IP address,IP addresses]', # loc
185                   ]
186                 },
187     IPAddressRange => {
188         sort_order => 120,
189         selection_type => 0,
190
191         labels => [ 'Enter multiple IP address ranges',                          # loc
192                     'Enter one IP address range',                                # loc
193                     'Enter up to [quant,_1,IP address range,IP address ranges]', # loc
194                   ]
195                 },
196 );
197
198
199 my %BUILTIN_GROUPINGS;
200 my %FRIENDLY_LOOKUP_TYPES = ();
201
202 __PACKAGE__->RegisterLookupType( 'RT::Queue-RT::Ticket' => "Tickets", );    #loc
203 __PACKAGE__->RegisterLookupType( 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", ); #loc
204 __PACKAGE__->RegisterLookupType( 'RT::User'  => "Users", );                           #loc
205 __PACKAGE__->RegisterLookupType( 'RT::Queue'  => "Queues", );                         #loc
206 __PACKAGE__->RegisterLookupType( 'RT::Group' => "Groups", );                          #loc
207
208 __PACKAGE__->RegisterBuiltInGroupings(
209     'RT::Ticket'    => [ qw(Basics Dates Links People) ],
210     'RT::User'      => [ 'Identity', 'Access control', 'Location', 'Phones' ],
211 );
212
213 __PACKAGE__->AddRight( General => SeeCustomField         => 'View custom fields'); # loc
214 __PACKAGE__->AddRight( Admin   => AdminCustomField       => 'Create, modify and delete custom fields'); # loc
215 __PACKAGE__->AddRight( Admin   => AdminCustomFieldValues => 'Create, modify and delete custom fields values'); # loc
216 __PACKAGE__->AddRight( Staff   => ModifyCustomField      => 'Add, modify and delete custom field values for objects'); # loc
217
218 =head1 NAME
219
220   RT::CustomField_Overlay - overlay for RT::CustomField
221
222 =head1 DESCRIPTION
223
224 =head1 'CORE' METHODS
225
226 =head2 Create PARAMHASH
227
228 Create takes a hash of values and creates a row in the database:
229
230   varchar(200) 'Name'.
231   varchar(200) 'Type'.
232   int(11) 'MaxValues'.
233   varchar(255) 'Pattern'.
234   varchar(255) 'Description'.
235   int(11) 'SortOrder'.
236   varchar(255) 'LookupType'.
237   smallint(6) 'Disabled'.
238
239 C<LookupType> is generally the result of either
240 C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType>.
241
242 =cut
243
244 sub Create {
245     my $self = shift;
246     my %args = (
247         Name        => '',
248         Type        => '',
249         MaxValues   => 0,
250         Pattern     => '',
251         Description => '',
252         Disabled    => 0,
253         LookupType  => '',
254         LinkValueTo => '',
255         IncludeContentForValue => '',
256         @_,
257     );
258
259     unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) {
260         return (0, $self->loc('Permission Denied'));
261     }
262
263     if ( $args{TypeComposite} ) {
264         @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
265     }
266     elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) {
267         # old style Type string
268         $args{'MaxValues'} = $1 ? 1 : 0;
269     }
270     $args{'MaxValues'} = int $args{'MaxValues'};
271
272     if ( !exists $args{'Queue'}) {
273     # do nothing -- things below are strictly backward compat
274     }
275     elsif (  ! $args{'Queue'} ) {
276         unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
277             return ( 0, $self->loc('Permission Denied') );
278         }
279         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
280     }
281     else {
282         my $queue = RT::Queue->new($self->CurrentUser);
283         $queue->Load($args{'Queue'});
284         unless ($queue->Id) {
285             return (0, $self->loc("Queue not found"));
286         }
287         unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
288             return ( 0, $self->loc('Permission Denied') );
289         }
290         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
291         $args{'Queue'} = $queue->Id;
292     }
293
294     my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} );
295     return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok;
296
297     if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) {
298         $RT::Logger->debug("Support for 'multiple' Texts or Comboboxes is not implemented");
299         $args{'MaxValues'} = 1;
300     }
301
302     if ( $args{'RenderType'} ||= undef ) {
303         my $composite = join '-', @args{'Type', 'MaxValues'};
304         return (0, $self->loc("This custom field has no Render Types"))
305             unless $self->HasRenderTypes( $composite );
306
307         if ( $args{'RenderType'} eq $self->DefaultRenderType( $composite ) ) {
308             $args{'RenderType'} = undef;
309         } else {
310             return (0, $self->loc("Invalid Render Type") )
311                 unless grep $_ eq  $args{'RenderType'}, $self->RenderTypes( $composite );
312         }
313     }
314
315     $args{'ValuesClass'} = undef if ($args{'ValuesClass'} || '') eq 'RT::CustomFieldValues';
316     if ( $args{'ValuesClass'} ||= undef ) {
317         return (0, $self->loc("This Custom Field can not have list of values"))
318             unless $self->IsSelectionType( $args{'Type'} );
319
320         unless ( $self->ValidateValuesClass( $args{'ValuesClass'} ) ) {
321             return (0, $self->loc("Invalid Custom Field values source"));
322         }
323     }
324
325     $args{'Disabled'} ||= 0;
326
327     (my $rv, $msg) = $self->SUPER::Create(
328         Name        => $args{'Name'},
329         Type        => $args{'Type'},
330         RenderType  => $args{'RenderType'},
331         MaxValues   => $args{'MaxValues'},
332         Pattern     => $args{'Pattern'},
333         BasedOn     => $args{'BasedOn'},
334         ValuesClass => $args{'ValuesClass'},
335         Description => $args{'Description'},
336         Disabled    => $args{'Disabled'},
337         LookupType  => $args{'LookupType'},
338     );
339
340     if ($rv) {
341         if ( exists $args{'LinkValueTo'}) {
342             $self->SetLinkValueTo($args{'LinkValueTo'});
343         }
344
345         if ( exists $args{'IncludeContentForValue'}) {
346             $self->SetIncludeContentForValue($args{'IncludeContentForValue'});
347         }
348
349         return ($rv, $msg) unless exists $args{'Queue'};
350
351         # Compat code -- create a new ObjectCustomField mapping
352         my $OCF = RT::ObjectCustomField->new( $self->CurrentUser );
353         $OCF->Create(
354             CustomField => $self->Id,
355             ObjectId => $args{'Queue'},
356         );
357     }
358
359     return ($rv, $msg);
360 }
361
362 =head2 Load ID/NAME
363
364 Load a custom field.  If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
365
366 =cut
367
368 sub Load {
369     my $self = shift;
370     my $id = shift || '';
371
372     if ( $id =~ /^\d+$/ ) {
373         return $self->SUPER::Load( $id );
374     } else {
375         return $self->LoadByName( Name => $id );
376     }
377 }
378
379
380
381 =head2 LoadByName Name => C<NAME>, [...]
382
383 Loads the Custom field named NAME.  As other optional parameters, takes:
384
385 =over
386
387 =item LookupType => C<LOOKUPTYPE>
388
389 The type of Custom Field to look for; while this parameter is not
390 required, it is highly suggested, or you may not find the Custom Field
391 you are expecting.  It should be passed a C<LookupType> such as
392 L<RT::Ticket/CustomFieldLookupType> or
393 L<RT::User/CustomFieldLookupType>.
394
395 =item ObjectType => C<CLASS>
396
397 The class of object that the custom field is applied to.  This can be
398 intuited from the provided C<LookupType>.
399
400 =item ObjectId => C<ID>
401
402 limits the custom field search to one applied to the relevant id.  For
403 example, if a C<LookupType> of C<< RT::Ticket->CustomFieldLookupType >>
404 is used, this is which Queue the CF must be applied to.  Pass 0 to only
405 search custom fields that are applied globally.
406
407 =item IncludeDisabled => C<BOOLEAN>
408
409 Whether it should return Disabled custom fields if they match; defaults
410 to on, though non-Disabled custom fields are returned preferentially.
411
412 =item IncludeGlobal => C<BOOLEAN>
413
414 Whether to also search global custom fields, even if a value is provided
415 for C<ObjectId>; defaults to off.  Non-global custom fields are returned
416 preferentially.
417
418 =back
419
420 For backwards compatibility, a value passed for C<Queue> is equivalent
421 to specifying a C<LookupType> of L<RT::Ticket/CustomFieldLookupType>,
422 and a C<ObjectId> of the value passed as C<Queue>.
423
424 If multiple custom fields match the above constraints, the first
425 according to C<SortOrder> will be returned; ties are broken by C<id>,
426 lowest-first.
427
428 =head2 LoadNameAndQueue
429
430 =head2 LoadByNameAndQueue
431
432 Deprecated alternate names for L</LoadByName>.
433
434 =cut
435
436 # Compatibility for API change after 3.0 beta 1
437 *LoadNameAndQueue = \&LoadByName;
438 # Change after 3.4 beta.
439 *LoadByNameAndQueue = \&LoadByName;
440
441 sub LoadByName {
442     my $self = shift;
443     my %args = (
444         Name       => undef,
445         LookupType => undef,
446         ObjectType => undef,
447         ObjectId   => undef,
448
449         IncludeDisabled => 1,
450         IncludeGlobal   => 0,
451
452         # Back-compat
453         Queue => undef,
454
455         @_,
456     );
457
458     unless ( defined $args{'Name'} && length $args{'Name'} ) {
459         $RT::Logger->error("Couldn't load Custom Field without Name");
460         return wantarray ? (0, $self->loc("No name provided")) : 0;
461     }
462
463     if ( defined $args{'Queue'} ) {
464         # Set a LookupType for backcompat, otherwise we'll calculate
465         # one of RT::Queue from your ContextObj.  Older code was relying
466         # on us defaulting to RT::Queue-RT::Ticket in old LimitToQueue call.
467         $args{LookupType} ||= 'RT::Queue-RT::Ticket';
468         $args{ObjectId}   //= delete $args{Queue};
469     }
470
471     # Default the ObjectType to the top category of the LookupType; it's
472     # what the CFs are assigned on.
473     $args{ObjectType} ||= $1 if $args{LookupType} and $args{LookupType} =~ /^([^-]+)/;
474
475     # Resolve the ObjectId/ObjectType; this is necessary to properly
476     # limit ObjectId, and also possibly useful to set a ContextObj if we
477     # are currently lacking one.  It is not strictly necessary if we
478     # have a context object and were passed a numeric ObjectId, but it
479     # cannot hurt to verify its sanity.  Skip if we have a false
480     # ObjectId, which means "global", or if we lack an ObjectType
481     if ($args{ObjectId} and $args{ObjectType}) {
482         my ($obj, $ok, $msg);
483         eval {
484             $obj = $args{ObjectType}->new( $self->CurrentUser );
485             ($ok, $msg) = $obj->Load( $args{ObjectId} );
486         };
487
488         if ($ok) {
489             $args{ObjectId} = $obj->id;
490             $self->SetContextObject( $obj )
491                 unless $self->ContextObject;
492         } else {
493             $RT::Logger->warning("Failed to load $args{ObjectType} '$args{ObjectId}'");
494             if ($args{IncludeGlobal}) {
495                 # Fall back to acting like we were only asked about the
496                 # global case
497                 $args{ObjectId} = 0;
498             } else {
499                 # If they didn't also want global results, there's no
500                 # point in searching; abort
501                 return wantarray ? (0, $self->loc("Not found")) : 0;
502             }
503         }
504     } elsif (not $args{ObjectType} and $args{ObjectId}) {
505         # If we skipped out on the above due to lack of ObjectType, make
506         # sure we clear out ObjectId of anything lingering
507         $RT::Logger->warning("No LookupType or ObjectType passed; ignoring ObjectId");
508         delete $args{ObjectId};
509     }
510
511     my $CFs = RT::CustomFields->new( $self->CurrentUser );
512     $CFs->SetContextObject( $self->ContextObject );
513     my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
514     $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
515
516     # The context object may be a ticket, for example, as context for a
517     # queue CF.  The valid lookup types are thus the entire set of
518     # ACLEquivalenceObjects for the context object.
519     $args{LookupType} ||= [
520         map {$_->CustomFieldLookupType}
521             ($self->ContextObject, $self->ContextObject->ACLEquivalenceObjects) ]
522         if $self->ContextObject;
523
524     # Apply LookupType limits
525     $args{LookupType} = [ $args{LookupType} ]
526         if $args{LookupType} and not ref($args{LookupType});
527     $CFs->Limit( FIELD => "LookupType", OPERATOR => "IN", VALUE => $args{LookupType} )
528         if $args{LookupType};
529
530     # Default to by SortOrder and id; this mirrors the standard ordering
531     # of RT::CustomFields (minus the Name, which is guaranteed to be
532     # fixed)
533     my @order = (
534         { FIELD => 'SortOrder',
535           ORDER => 'ASC' },
536         { FIELD => 'id',
537           ORDER => 'ASC' },
538     );
539
540     if (defined $args{ObjectId}) {
541         # The join to OCFs is distinct -- either we have a global
542         # application or an objectid match, but never both.  Even if
543         # this were not the case, we care only for the first row.
544         my $ocfs = $CFs->_OCFAlias( Distinct => 1);
545         if ($args{IncludeGlobal}) {
546             $CFs->Limit(
547                 ALIAS    => $ocfs,
548                 FIELD    => 'ObjectId',
549                 OPERATOR => 'IN',
550                 VALUE    => [ $args{ObjectId}, 0 ],
551             );
552             # Find the queue-specific first
553             unshift @order, { ALIAS => $ocfs, FIELD => "ObjectId", ORDER => "DESC" };
554         } else {
555             $CFs->Limit(
556                 ALIAS => $ocfs,
557                 FIELD => 'ObjectId',
558                 VALUE => $args{ObjectId},
559             );
560         }
561     }
562
563     if ($args{IncludeDisabled}) {
564         # Load disabled fields, but return them only as a last resort.
565         # This goes at the front of @order, as we prefer the
566         # non-disabled global CF to the disabled Queue-specific CF.
567         $CFs->FindAllRows;
568         unshift @order, { FIELD => "Disabled", ORDER => 'ASC' };
569     }
570
571     # Apply the above orderings
572     $CFs->OrderByCols( @order );
573
574     # We only want one entry.
575     $CFs->RowsPerPage(1);
576
577     # version before 3.8 just returns 0, so we need to test if wantarray to be
578     # backward compatible.
579     return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
580
581     return $self->LoadById( $first->id );
582 }
583
584
585
586
587 =head2 Custom field values
588
589 =head3 Values FIELD
590
591 Return a object (collection) of all acceptable values for this Custom Field.
592 Class of the object can vary and depends on the return value
593 of the C<ValuesClass> method.
594
595 =cut
596
597 *ValuesObj = \&Values;
598
599 sub Values {
600     my $self = shift;
601
602     my $class = $self->ValuesClass;
603     if ( $class ne 'RT::CustomFieldValues') {
604         $class->require or die "Can't load $class: $@";
605     }
606     my $cf_values = $class->new( $self->CurrentUser );
607     # if the user has no rights, return an empty object
608     if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
609         $cf_values->LimitToCustomField( $self->Id );
610     } else {
611         $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
612     }
613     return ($cf_values);
614 }
615
616
617 =head3 AddValue HASH
618
619 Create a new value for this CustomField.  Takes a paramhash containing the elements Name, Description and SortOrder
620
621 =cut
622
623 sub AddValue {
624     my $self = shift;
625     my %args = @_;
626
627     unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) {
628         return (0, $self->loc('Permission Denied'));
629     }
630
631     # allow zero value
632     if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
633         return (0, $self->loc("Can't add a custom field value without a name"));
634     }
635
636     my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
637     return $newval->Create( %args, CustomField => $self->Id );
638 }
639
640
641
642
643 =head3 DeleteValue ID
644
645 Deletes a value from this custom field by id.
646
647 Does not remove this value for any article which has had it selected
648
649 =cut
650
651 sub DeleteValue {
652     my $self = shift;
653     my $id = shift;
654     unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) {
655         return (0, $self->loc('Permission Denied'));
656     }
657
658     my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
659     $val_to_del->Load( $id );
660     unless ( $val_to_del->Id ) {
661         return (0, $self->loc("Couldn't find that value"));
662     }
663     unless ( $val_to_del->CustomField == $self->Id ) {
664         return (0, $self->loc("That is not a value for this custom field"));
665     }
666
667     my $retval = $val_to_del->Delete;
668     unless ( $retval ) {
669         return (0, $self->loc("Custom field value could not be deleted"));
670     }
671     return ($retval, $self->loc("Custom field value deleted"));
672 }
673
674
675 =head2 ValidateQueue Queue
676
677 Make sure that the name specified is valid
678
679 =cut
680
681 sub ValidateName {
682     my $self = shift;
683     my $value = shift;
684
685     return 0 unless length $value;
686
687     return $self->SUPER::ValidateName($value);
688 }
689
690 =head2 ValidateQueue Queue
691
692 Make sure that the queue specified is a valid queue name
693
694 =cut
695
696 sub ValidateQueue {
697     my $self = shift;
698     my $id = shift;
699
700     return undef unless defined $id;
701     # 0 means "Global" null would _not_ be ok.
702     return 1 if $id eq '0';
703
704     my $q = RT::Queue->new( RT->SystemUser );
705     $q->Load( $id );
706     return undef unless $q->id;
707     return 1;
708 }
709
710
711
712 =head2 Types 
713
714 Retuns an array of the types of CustomField that are supported
715
716 =cut
717
718 sub Types {
719     return (sort {(($FieldTypes{$a}{sort_order}||999) <=> ($FieldTypes{$b}{sort_order}||999)) or ($a cmp $b)} keys %FieldTypes);
720 }
721
722
723 =head2 IsSelectionType 
724
725 Retuns a boolean value indicating whether the C<Values> method makes sense
726 to this Custom Field.
727
728 =cut
729
730 sub IsSelectionType {
731     my $self = shift;
732     my $type = @_? shift : $self->Type;
733     return undef unless $type;
734     return $FieldTypes{$type}->{selection_type};
735 }
736
737
738
739 =head2 IsExternalValues
740
741 =cut
742
743 sub IsExternalValues {
744     my $self = shift;
745     return 0 unless $self->IsSelectionType( @_ );
746     return $self->ValuesClass eq 'RT::CustomFieldValues'? 0 : 1;
747 }
748
749 sub ValuesClass {
750     my $self = shift;
751     return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues';
752 }
753
754 sub SetValuesClass {
755     my $self = shift;
756     my $class = shift || 'RT::CustomFieldValues';
757     
758     if ( $class eq 'RT::CustomFieldValues' ) {
759         return $self->_Set( Field => 'ValuesClass', Value => undef, @_ );
760     }
761
762     return (0, $self->loc("This Custom Field can not have list of values"))
763         unless $self->IsSelectionType;
764
765     unless ( $self->ValidateValuesClass( $class ) ) {
766         return (0, $self->loc("Invalid Custom Field values source"));
767     }
768     return $self->_Set( Field => 'ValuesClass', Value => $class, @_ );
769 }
770
771 sub ValidateValuesClass {
772     my $self = shift;
773     my $class = shift;
774
775     return 1 if !$class || $class eq 'RT::CustomFieldValues';
776     return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesSources');
777     return undef;
778 }
779
780
781 =head2 FriendlyType [TYPE, MAX_VALUES]
782
783 Returns a localized human-readable version of the custom field type.
784 If a custom field type is specified as the parameter, the friendly type for that type will be returned
785
786 =cut
787
788 sub FriendlyType {
789     my $self = shift;
790
791     my $type = @_ ? shift : $self->Type;
792     my $max  = @_ ? shift : $self->MaxValues;
793     $max = 0 unless $max;
794
795     if (my $friendly_type = $FieldTypes{$type}->{labels}->[$max>2 ? 2 : $max]) {
796         return ( $self->loc( $friendly_type, $max ) );
797     }
798     else {
799         return ( $self->loc( $type ) );
800     }
801 }
802
803 sub FriendlyTypeComposite {
804     my $self = shift;
805     my $composite = shift || $self->TypeComposite;
806     return $self->FriendlyType(split(/-/, $composite, 2));
807 }
808
809
810 =head2 ValidateType TYPE
811
812 Takes a single string. returns true if that string is a value
813 type of custom field
814
815
816 =cut
817
818 sub ValidateType {
819     my $self = shift;
820     my $type = shift;
821
822     if ( $type =~ s/(?:Single|Multiple)$// ) {
823         RT->Deprecated(
824             Arguments => "suffix 'Single' or 'Multiple'",
825             Instead   => "MaxValues",
826             Remove    => "4.4",
827         );
828     }
829
830     if ( $FieldTypes{$type} ) {
831         return 1;
832     }
833     else {
834         return undef;
835     }
836 }
837
838
839 sub SetType {
840     my $self = shift;
841     my $type = shift;
842     if ($type =~ s/(?:(Single)|Multiple)$//) {
843         RT->Deprecated(
844             Arguments => "suffix 'Single' or 'Multiple'",
845             Instead   => "MaxValues",
846             Remove    => "4.4",
847         );
848         $self->SetMaxValues($1 ? 1 : 0);
849     }
850     $self->_Set(Field => 'Type', Value =>$type);
851 }
852
853 =head2 SetPattern STRING
854
855 Takes a single string representing a regular expression.  Performs basic
856 validation on that regex, and sets the C<Pattern> field for the CF if it
857 is valid.
858
859 =cut
860
861 sub SetPattern {
862     my $self = shift;
863     my $regex = shift;
864
865     my ($ok, $msg) = $self->_IsValidRegex($regex);
866     if ($ok) {
867         return $self->_Set(Field => 'Pattern', Value => $regex);
868     }
869     else {
870         return (0, $self->loc("Invalid pattern: [_1]", $msg));
871     }
872 }
873
874 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
875
876 Tests if the string contains an invalid regex.
877
878 =cut
879
880 sub _IsValidRegex {
881     my $self  = shift;
882     my $regex = shift or return (1, 'valid');
883
884     local $^W; local $@;
885     local $SIG{__DIE__} = sub { 1 };
886     local $SIG{__WARN__} = sub { 1 };
887
888     if (eval { qr/$regex/; 1 }) {
889         return (1, 'valid');
890     }
891
892     my $err = $@;
893     $err =~ s{[,;].*}{};    # strip debug info from error
894     chomp $err;
895     return (0, $err);
896 }
897
898
899 =head2 SingleValue
900
901 Returns true if this CustomField only accepts a single value. 
902 Returns false if it accepts multiple values
903
904 =cut
905
906 sub SingleValue {
907     my $self = shift;
908     if (($self->MaxValues||0) == 1) {
909         return 1;
910     } 
911     else {
912         return undef;
913     }
914 }
915
916 sub UnlimitedValues {
917     my $self = shift;
918     if (($self->MaxValues||0) == 0) {
919         return 1;
920     } 
921     else {
922         return undef;
923     }
924 }
925
926
927 =head2 ACLEquivalenceObjects
928
929 Returns list of objects via which users can get rights on this custom field. For custom fields
930 these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
931
932 =cut
933
934 sub ACLEquivalenceObjects {
935     my $self = shift;
936
937     my $ctx = $self->ContextObject
938         or return;
939     return ($ctx, $ctx->ACLEquivalenceObjects);
940 }
941
942 =head2 ContextObject and SetContextObject
943
944 Set or get a context for this object. It can be ticket, queue or another
945 object this CF added to. Used for ACL control, for example
946 SeeCustomField can be granted on queue level to allow people to see all
947 fields added to the queue.
948
949 =cut
950
951 sub SetContextObject {
952     my $self = shift;
953     return $self->{'context_object'} = shift;
954 }
955   
956 sub ContextObject {
957     my $self = shift;
958     return $self->{'context_object'};
959 }
960
961 sub ValidContextType {
962     my $self = shift;
963     my $class = shift;
964
965     my %valid;
966     $valid{$_}++ for split '-', $self->LookupType;
967     delete $valid{'RT::Transaction'};
968
969     return $valid{$class};
970 }
971
972 =head2 LoadContextObject
973
974 Takes an Id for a Context Object and loads the right kind of RT::Object
975 for this particular Custom Field (based on the LookupType) and returns it.
976 This is a good way to ensure you don't try to use a Queue as a Context
977 Object on a User Custom Field.
978
979 =cut
980
981 sub LoadContextObject {
982     my $self = shift;
983     my $type = shift;
984     my $contextid = shift;
985
986     unless ( $self->ValidContextType($type) ) {
987         RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id);
988         return;
989     }
990
991     my $context_object = $type->new( $self->CurrentUser );
992     my ($id, $msg) = $context_object->LoadById( $contextid );
993     unless ( $id ) {
994         RT->Logger->debug("Invalid ContextObject id: $msg");
995         return;
996     }
997     return $context_object;
998 }
999
1000 =head2 ValidateContextObject
1001
1002 Ensure that a given ContextObject applies to this Custom Field.  For
1003 custom fields that are assigned to Queues or to Classes, this checks
1004 that the Custom Field is actually added to that object.  For Global
1005 Custom Fields, it returns true as long as the Object is of the right
1006 type, because you may be using your permissions on a given Queue of
1007 Class to see a Global CF.  For CFs that are only added globally, you
1008 don't need a ContextObject.
1009
1010 =cut
1011
1012 sub ValidateContextObject {
1013     my $self = shift;
1014     my $object = shift;
1015
1016     return 1 if $self->IsGlobal;
1017
1018     # global only custom fields don't have objects
1019     # that should be used as context objects.
1020     return if $self->IsOnlyGlobal;
1021
1022     # Otherwise, make sure we weren't passed a user object that we're
1023     # supposed to treat as a queue.
1024     return unless $self->ValidContextType(ref $object);
1025
1026     # Check that it is added correctly
1027     my ($added_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects);
1028     return unless $added_to;
1029     return $self->IsAdded($added_to->id);
1030 }
1031
1032
1033 sub _Set {
1034     my $self = shift;
1035
1036     unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1037         return ( 0, $self->loc('Permission Denied') );
1038     }
1039     return $self->SUPER::_Set( @_ );
1040
1041 }
1042
1043
1044
1045 =head2 _Value
1046
1047 Takes the name of a table column.
1048 Returns its value as a string, if the user passes an ACL check
1049
1050 =cut
1051
1052 sub _Value {
1053     my $self  = shift;
1054     return undef unless $self->id;
1055
1056     # we need to do the rights check
1057     unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
1058         $RT::Logger->debug(
1059             "Permission denied. User #". $self->CurrentUser->id
1060             ." has no SeeCustomField right on CF #". $self->id
1061         );
1062         return (undef);
1063     }
1064     return $self->__Value( @_ );
1065 }
1066
1067
1068 =head2 SetDisabled
1069
1070 Takes a boolean.
1071 1 will cause this custom field to no longer be avaialble for objects.
1072 0 will re-enable this field.
1073
1074 =cut
1075
1076
1077 =head2 SetTypeComposite
1078
1079 Set this custom field's type and maximum values as a composite value
1080
1081 =cut
1082
1083 sub SetTypeComposite {
1084     my $self = shift;
1085     my $composite = shift;
1086
1087     my $old = $self->TypeComposite;
1088
1089     my ($type, $max_values) = split(/-/, $composite, 2);
1090     if ( $type ne $self->Type ) {
1091         my ($status, $msg) = $self->SetType( $type );
1092         return ($status, $msg) unless $status;
1093     }
1094     if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
1095         my ($status, $msg) = $self->SetMaxValues( $max_values );
1096         return ($status, $msg) unless $status;
1097     }
1098     my $render = $self->RenderType;
1099     if ( $render and not grep { $_ eq $render } $self->RenderTypes ) {
1100         # We switched types and our render type is no longer valid, so unset it
1101         # and use the default
1102         $self->SetRenderType( undef );
1103     }
1104     return 1, $self->loc(
1105         "Type changed from '[_1]' to '[_2]'",
1106         $self->FriendlyTypeComposite( $old ),
1107         $self->FriendlyTypeComposite( $composite ),
1108     );
1109 }
1110
1111 =head2 TypeComposite
1112
1113 Returns a composite value composed of this object's type and maximum values
1114
1115 =cut
1116
1117
1118 sub TypeComposite {
1119     my $self = shift;
1120     return join '-', ($self->Type || ''), ($self->MaxValues || 0);
1121 }
1122
1123 =head2 TypeComposites
1124
1125 Returns an array of all possible composite values for custom fields.
1126
1127 =cut
1128
1129 sub TypeComposites {
1130     my $self = shift;
1131     return grep !/(?:[Tt]ext|Combobox|Date|DateTime)-0/, map { ("$_-1", "$_-0") } $self->Types;
1132 }
1133
1134 =head2 RenderType
1135
1136 Returns the type of form widget to render for this custom field.  Currently
1137 this only affects fields which return true for L</HasRenderTypes>. 
1138
1139 =cut
1140
1141 sub RenderType {
1142     my $self = shift;
1143     return '' unless $self->HasRenderTypes;
1144
1145     return $self->_Value( 'RenderType', @_ )
1146         || $self->DefaultRenderType;
1147 }
1148
1149 =head2 SetRenderType TYPE
1150
1151 Sets this custom field's render type.
1152
1153 =cut
1154
1155 sub SetRenderType {
1156     my $self = shift;
1157     my $type = shift;
1158     return (0, $self->loc("This custom field has no Render Types"))
1159         unless $self->HasRenderTypes;
1160
1161     if ( !$type || $type eq $self->DefaultRenderType ) {
1162         return $self->_Set( Field => 'RenderType', Value => undef, @_ );
1163     }
1164
1165     if ( not grep { $_ eq $type } $self->RenderTypes ) {
1166         return (0, $self->loc("Invalid Render Type for custom field of type [_1]",
1167                                 $self->FriendlyType));
1168     }
1169
1170     return $self->_Set( Field => 'RenderType', Value => $type, @_ );
1171 }
1172
1173 =head2 DefaultRenderType [TYPE COMPOSITE]
1174
1175 Returns the default render type for this custom field's type or the TYPE
1176 COMPOSITE specified as an argument.
1177
1178 =cut
1179
1180 sub DefaultRenderType {
1181     my $self = shift;
1182     my $composite    = @_ ? shift : $self->TypeComposite;
1183     my ($type, $max) = split /-/, $composite, 2;
1184     return unless $type and $self->HasRenderTypes($composite);
1185     return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0];
1186 }
1187
1188 =head2 HasRenderTypes [TYPE_COMPOSITE]
1189
1190 Returns a boolean value indicating whether the L</RenderTypes> and
1191 L</RenderType> methods make sense for this custom field.
1192
1193 Currently true only for type C<Select>.
1194
1195 =cut
1196
1197 sub HasRenderTypes {
1198     my $self = shift;
1199     my ($type, $max) = split /-/, (@_ ? shift : $self->TypeComposite), 2;
1200     return undef unless $type;
1201     return defined $FieldTypes{$type}->{render_types}
1202         ->{ $max == 1 ? 'single' : 'multiple' };
1203 }
1204
1205 =head2 RenderTypes [TYPE COMPOSITE]
1206
1207 Returns the valid render types for this custom field's type or the TYPE
1208 COMPOSITE specified as an argument.
1209
1210 =cut
1211
1212 sub RenderTypes {
1213     my $self = shift;
1214     my $composite    = @_ ? shift : $self->TypeComposite;
1215     my ($type, $max) = split /-/, $composite, 2;
1216     return unless $type and $self->HasRenderTypes($composite);
1217     return @{$FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }};
1218 }
1219
1220 =head2 SetLookupType
1221
1222 Autrijus: care to doc how LookupTypes work?
1223
1224 =cut
1225
1226 sub SetLookupType {
1227     my $self = shift;
1228     my $lookup = shift;
1229     if ( $lookup ne $self->LookupType ) {
1230         # Okay... We need to invalidate our existing relationships
1231         RT::ObjectCustomField->new($self->CurrentUser)->DeleteAll( CustomField => $self );
1232     }
1233     return $self->_Set(Field => 'LookupType', Value =>$lookup);
1234 }
1235
1236 =head2 LookupTypes
1237
1238 Returns an array of LookupTypes available
1239
1240 =cut
1241
1242
1243 sub LookupTypes {
1244     my $self = shift;
1245     return sort keys %FRIENDLY_LOOKUP_TYPES;
1246 }
1247
1248 =head2 FriendlyLookupType
1249
1250 Returns a localized description of the type of this custom field
1251
1252 =cut
1253
1254 sub FriendlyLookupType {
1255     my $self = shift;
1256     my $lookup = shift || $self->LookupType;
1257
1258     return ($self->loc( $FRIENDLY_LOOKUP_TYPES{$lookup} ))
1259         if defined $FRIENDLY_LOOKUP_TYPES{$lookup};
1260
1261     my @types = map { s/^RT::// ? $self->loc($_) : $_ }
1262       grep { defined and length }
1263       split( /-/, $lookup )
1264       or return;
1265
1266     state $LocStrings = [
1267         "[_1] objects",            # loc
1268         "[_1]'s [_2] objects",        # loc
1269         "[_1]'s [_2]'s [_3] objects",   # loc
1270     ];
1271     return ( $self->loc( $LocStrings->[$#types], @types ) );
1272 }
1273
1274 =head1 RecordClassFromLookupType
1275
1276 Returns the type of Object referred to by ObjectCustomFields' ObjectId column
1277
1278 Optionally takes a LookupType to use instead of using the value on the loaded
1279 record.  In this case, the method may be called on the class instead of an
1280 object.
1281
1282 =cut
1283
1284 sub RecordClassFromLookupType {
1285     my $self = shift;
1286     my $type = shift || $self->LookupType;
1287     my ($class) = ($type =~ /^([^-]+)/);
1288     unless ( $class ) {
1289         if (blessed($self) and $self->LookupType eq $type) {
1290             $RT::Logger->error(
1291                 "Custom Field #". $self->id
1292                 ." has incorrect LookupType '$type'"
1293             );
1294         } else {
1295             RT->Logger->error("Invalid LookupType passed as argument: $type");
1296         }
1297         return undef;
1298     }
1299     return $class;
1300 }
1301
1302 =head1 ObjectTypeFromLookupType
1303
1304 Returns the ObjectType used in ObjectCustomFieldValues rows for this CF
1305
1306 Optionally takes a LookupType to use instead of using the value on the loaded
1307 record.  In this case, the method may be called on the class instead of an
1308 object.
1309
1310 =cut
1311
1312 sub ObjectTypeFromLookupType {
1313     my $self = shift;
1314     my $type = shift || $self->LookupType;
1315     my ($class) = ($type =~ /([^-]+)$/);
1316     unless ( $class ) {
1317         if (blessed($self) and $self->LookupType eq $type) {
1318             $RT::Logger->error(
1319                 "Custom Field #". $self->id
1320                 ." has incorrect LookupType '$type'"
1321             );
1322         } else {
1323             RT->Logger->error("Invalid LookupType passed as argument: $type");
1324         }
1325         return undef;
1326     }
1327     return $class;
1328 }
1329
1330 sub CollectionClassFromLookupType {
1331     my $self = shift;
1332
1333     my $record_class = $self->RecordClassFromLookupType;
1334     return undef unless $record_class;
1335
1336     my $collection_class;
1337     if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
1338         $collection_class = $record_class.'Collection';
1339     } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
1340         $collection_class = $record_class.'es';
1341     } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
1342         $collection_class = $record_class.'s';
1343     } else {
1344         $RT::Logger->error("Can not find a collection class for record class '$record_class'");
1345         return undef;
1346     }
1347     return $collection_class;
1348 }
1349
1350 =head2 Groupings
1351
1352 Returns a (sorted and lowercased) list of the groupings in which this custom
1353 field appears.
1354
1355 If called on a loaded object, the returned list is limited to groupings which
1356 apply to the record class this CF applies to (L</RecordClassFromLookupType>).
1357
1358 If passed a loaded object or a class name, the returned list is limited to
1359 groupings which apply to the class of the object or the specified class.
1360
1361 If called on an unloaded object, all potential groupings are returned.
1362
1363 =cut
1364
1365 sub Groupings {
1366     my $self = shift;
1367     my $record_class = $self->_GroupingClass(shift);
1368
1369     my $config = RT->Config->Get('CustomFieldGroupings');
1370        $config = {} unless ref($config) eq 'HASH';
1371
1372     my @groups;
1373     if ( $record_class ) {
1374         push @groups, sort {lc($a) cmp lc($b)} keys %{ $BUILTIN_GROUPINGS{$record_class} || {} };
1375         if ( ref($config->{$record_class} ||= []) eq "ARRAY") {
1376             my @order = @{ $config->{$record_class} };
1377             while (@order) {
1378                 push @groups, shift(@order);
1379                 shift(@order);
1380             }
1381         } else {
1382             @groups = sort {lc($a) cmp lc($b)} keys %{ $config->{$record_class} };
1383         }
1384     } else {
1385         my %all = (%$config, %BUILTIN_GROUPINGS);
1386         @groups = sort {lc($a) cmp lc($b)} map {$self->Groupings($_)} grep {$_} keys(%all);
1387     }
1388
1389     my %seen;
1390     return
1391         grep defined && length && !$seen{lc $_}++,
1392         @groups;
1393 }
1394
1395 =head2 CustomGroupings
1396
1397 Identical to L</Groupings> but filters out built-in groupings from the the
1398 returned list.
1399
1400 =cut
1401
1402 sub CustomGroupings {
1403     my $self = shift;
1404     my $record_class = $self->_GroupingClass(shift);
1405     return grep !$BUILTIN_GROUPINGS{$record_class}{$_}, $self->Groupings( $record_class );
1406 }
1407
1408 sub _GroupingClass {
1409     my $self    = shift;
1410     my $record  = shift;
1411
1412     my $record_class = ref($record) || $record || '';
1413     $record_class = $self->RecordClassFromLookupType
1414         if !$record_class and blessed($self) and $self->id;
1415
1416     return $record_class;
1417 }
1418
1419 =head2 RegisterBuiltInGroupings
1420
1421 Registers groupings to be considered a fundamental part of RT, either via use
1422 in core RT or via an extension.  These groupings must be rendered explicitly in
1423 Mason by specific calls to F</Elements/ShowCustomFields> and
1424 F</Elements/EditCustomFields>.  They will not show up automatically on normal
1425 display pages like configured custom groupings.
1426
1427 Takes a set of key-value pairs of class names (valid L<RT::Record> subclasses)
1428 and array refs of grouping names to consider built-in.
1429
1430 If a class already contains built-in groupings (such as L<RT::Ticket> and
1431 L<RT::User>), new groupings are appended.
1432
1433 =cut
1434
1435 sub RegisterBuiltInGroupings {
1436     my $self = shift;
1437     my %new  = @_;
1438
1439     while (my ($k,$v) = each %new) {
1440         $v = [$v] unless ref($v) eq 'ARRAY';
1441         $BUILTIN_GROUPINGS{$k} = {
1442             %{$BUILTIN_GROUPINGS{$k} || {}},
1443             map { $_ => 1 } @$v
1444         };
1445     }
1446     $BUILTIN_GROUPINGS{''} = { map { %$_ } values %BUILTIN_GROUPINGS  };
1447 }
1448
1449 =head1 IsOnlyGlobal
1450
1451 Certain custom fields (users, groups) should only be added globally;
1452 codify that set here for reference.
1453
1454 =cut
1455
1456 sub IsOnlyGlobal {
1457     my $self = shift;
1458
1459     return ($self->LookupType =~ /^RT::(?:Group|User)/io);
1460
1461 }
1462 sub ApplyGlobally {
1463     RT->Deprecated(
1464         Instead   => "IsOnlyGlobal",
1465         Remove    => "4.4",
1466     );
1467     return shift->IsOnlyGlobal(@_);
1468 }
1469
1470 =head1 AddedTo
1471
1472 Returns collection with objects this custom field is added to.
1473 Class of the collection depends on L</LookupType>.
1474 See all L</NotAddedTo> .
1475
1476 Doesn't takes into account if object is added globally.
1477
1478 =cut
1479
1480 sub AddedTo {
1481     my $self = shift;
1482     return RT::ObjectCustomField->new( $self->CurrentUser )
1483         ->AddedTo( CustomField => $self );
1484 }
1485 sub AppliedTo {
1486     RT->Deprecated(
1487         Instead   => "AddedTo",
1488         Remove    => "4.4",
1489     );
1490     shift->AddedTo(@_);
1491 };
1492
1493 =head1 NotAddedTo
1494
1495 Returns collection with objects this custom field is not added to.
1496 Class of the collection depends on L</LookupType>.
1497 See all L</AddedTo> .
1498
1499 Doesn't take into account if the object is added globally.
1500
1501 =cut
1502
1503 sub NotAddedTo {
1504     my $self = shift;
1505     return RT::ObjectCustomField->new( $self->CurrentUser )
1506         ->NotAddedTo( CustomField => $self );
1507 }
1508 sub NotAppliedTo {
1509     RT->Deprecated(
1510         Instead   => "NotAddedTo",
1511         Remove    => "4.4",
1512     );
1513     shift->NotAddedTo(@_)
1514 };
1515
1516 =head2 IsAdded
1517
1518 Takes object id and returns corresponding L<RT::ObjectCustomField>
1519 record if this custom field is added to the object. Use 0 to check
1520 if custom field is added globally.
1521
1522 =cut
1523
1524 sub IsAdded {
1525     my $self = shift;
1526     my $id = shift;
1527     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1528     $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1529     return undef unless $ocf->id;
1530     return $ocf;
1531 }
1532 sub IsApplied {
1533     RT->Deprecated(
1534         Instead   => "IsAdded",
1535         Remove    => "4.4",
1536     );
1537     shift->IsAdded(@_);
1538 };
1539
1540 sub IsGlobal { return shift->IsAdded(0) }
1541
1542 =head2 IsAddedToAny
1543
1544 Returns true if custom field is applied to any object.
1545
1546 =cut
1547
1548 sub IsAddedToAny {
1549     my $self = shift;
1550     my $id = shift;
1551     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1552     $ocf->LoadByCols( CustomField => $self->id );
1553     return $ocf->id ? 1 : 0;
1554 }
1555
1556 =head2 AddToObject OBJECT
1557
1558 Add this custom field as a custom field for a single object, such as a queue or group.
1559
1560 Takes an object 
1561
1562 =cut
1563
1564 sub AddToObject {
1565     my $self  = shift;
1566     my $object = shift;
1567     my $id = $object->Id || 0;
1568
1569     unless (index($self->LookupType, ref($object)) == 0) {
1570         return ( 0, $self->loc('Lookup type mismatch') );
1571     }
1572
1573     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1574         return ( 0, $self->loc('Permission Denied') );
1575     }
1576
1577     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1578     my ( $oid, $msg ) = $ocf->Add(
1579         CustomField => $self->id, ObjectId => $id,
1580     );
1581     return ( $oid, $msg );
1582 }
1583
1584
1585 =head2 RemoveFromObject OBJECT
1586
1587 Remove this custom field  for a single object, such as a queue or group.
1588
1589 Takes an object 
1590
1591 =cut
1592
1593 sub RemoveFromObject {
1594     my $self = shift;
1595     my $object = shift;
1596     my $id = $object->Id || 0;
1597
1598     unless (index($self->LookupType, ref($object)) == 0) {
1599         return ( 0, $self->loc('Object type mismatch') );
1600     }
1601
1602     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1603         return ( 0, $self->loc('Permission Denied') );
1604     }
1605
1606     my $ocf = $self->IsAdded( $id );
1607     unless ( $ocf ) {
1608         return ( 0, $self->loc("This custom field cannot be added to that object") );
1609     }
1610
1611     # XXX: Delete doesn't return anything
1612     my ( $oid, $msg ) = $ocf->Delete;
1613     return ( $oid, $msg );
1614 }
1615
1616
1617 =head2 AddValueForObject HASH
1618
1619 Adds a custom field value for a record object of some kind. 
1620 Takes a param hash of 
1621
1622 Required:
1623
1624     Object
1625     Content
1626
1627 Optional:
1628
1629     LargeContent
1630     ContentType
1631
1632 =cut
1633
1634 sub AddValueForObject {
1635     my $self = shift;
1636     my %args = (
1637         Object       => undef,
1638         Content      => undef,
1639         LargeContent => undef,
1640         ContentType  => undef,
1641         @_
1642     );
1643     my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1644
1645     unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1646         return ( 0, $self->loc('Permission Denied') );
1647     }
1648
1649     unless ( $self->MatchPattern($args{'Content'}) ) {
1650         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1651     }
1652
1653     $RT::Handle->BeginTransaction;
1654
1655     if ( $self->MaxValues ) {
1656         my $current_values = $self->ValuesForObject($obj);
1657         my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1658
1659         # (The +1 is for the new value we're adding)
1660
1661         # If we have a set of current values and we've gone over the maximum
1662         # allowed number of values, we'll need to delete some to make room.
1663         # which former values are blown away is not guaranteed
1664
1665         while ($extra_values) {
1666             my $extra_item = $current_values->Next;
1667             unless ( $extra_item->id ) {
1668                 $RT::Logger->crit( "We were just asked to delete "
1669                     ."a custom field value that doesn't exist!" );
1670                 $RT::Handle->Rollback();
1671                 return (undef);
1672             }
1673             $extra_item->Delete;
1674             $extra_values--;
1675         }
1676     }
1677
1678     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1679     my ($val, $msg) = $newval->Create(
1680         ObjectType   => ref($obj),
1681         ObjectId     => $obj->Id,
1682         Content      => $args{'Content'},
1683         LargeContent => $args{'LargeContent'},
1684         ContentType  => $args{'ContentType'},
1685         CustomField  => $self->Id
1686     );
1687
1688     unless ($val) {
1689         $RT::Handle->Rollback();
1690         return ($val, $self->loc("Couldn't create record: [_1]", $msg));
1691     }
1692
1693     $RT::Handle->Commit();
1694     return ($val);
1695
1696 }
1697
1698
1699 sub _CanonicalizeValue {
1700     my $self = shift;
1701     my $args = shift;
1702
1703     my $type = $self->_Value('Type');
1704     return 1 unless $type;
1705
1706     my $method = '_CanonicalizeValue'. $type;
1707     return 1 unless $self->can($method);
1708     $self->$method($args);
1709 }
1710
1711 sub _CanonicalizeValueDateTime {
1712     my $self    = shift;
1713     my $args    = shift;
1714     my $DateObj = RT::Date->new( $self->CurrentUser );
1715     $DateObj->Set( Format => 'unknown',
1716                    Value  => $args->{'Content'} );
1717     $args->{'Content'} = $DateObj->ISO;
1718     return 1;
1719 }
1720
1721 # For date, we need to store Content as ISO date
1722 sub _CanonicalizeValueDate {
1723     my $self = shift;
1724     my $args = shift;
1725
1726     # in case user input date with time, let's omit it by setting timezone
1727     # to utc so "hour" won't affect "day"
1728     my $DateObj = RT::Date->new( $self->CurrentUser );
1729     $DateObj->Set( Format   => 'unknown',
1730                    Value    => $args->{'Content'},
1731                  );
1732     $args->{'Content'} = $DateObj->Date( Timezone => 'user' );
1733     return 1;
1734 }
1735
1736 sub _CanonicalizeValueIPAddress {
1737     my $self = shift;
1738     my $args = shift;
1739
1740     $args->{Content} = RT::ObjectCustomFieldValue->ParseIP( $args->{Content} );
1741     return (0, $self->loc("Content is not a valid IP address"))
1742         unless $args->{Content};
1743     return 1;
1744 }
1745
1746 sub _CanonicalizeValueIPAddressRange {
1747     my $self = shift;
1748     my $args = shift;
1749
1750     my $content = $args->{Content};
1751     $content .= "-".$args->{LargeContent} if $args->{LargeContent};
1752
1753     ($args->{Content}, $args->{LargeContent})
1754         = RT::ObjectCustomFieldValue->ParseIPRange( $content );
1755
1756     $args->{ContentType} = 'text/plain';
1757     return (0, $self->loc("Content is not a valid IP address range"))
1758         unless $args->{Content};
1759     return 1;
1760 }
1761
1762 =head2 MatchPattern STRING
1763
1764 Tests the incoming string against the Pattern of this custom field object
1765 and returns a boolean; returns true if the Pattern is empty.
1766
1767 =cut
1768
1769 sub MatchPattern {
1770     my $self = shift;
1771     my $regex = $self->Pattern or return 1;
1772
1773     return (( defined $_[0] ? $_[0] : '') =~ $regex);
1774 }
1775
1776
1777
1778
1779 =head2 FriendlyPattern
1780
1781 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1782 and localizing it.
1783
1784 =cut
1785
1786 sub FriendlyPattern {
1787     my $self = shift;
1788     my $regex = $self->Pattern;
1789
1790     return '' unless length $regex;
1791     if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1792         return '[' . $self->loc($1) . ']';
1793     }
1794     else {
1795         return $regex;
1796     }
1797 }
1798
1799
1800
1801
1802 =head2 DeleteValueForObject HASH
1803
1804 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1805
1806 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1807
1808 =cut
1809
1810 sub DeleteValueForObject {
1811     my $self = shift;
1812     my %args = ( Object => undef,
1813                  Content => undef,
1814                  Id => undef,
1815              @_ );
1816
1817
1818     unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1819         return (0, $self->loc('Permission Denied'));
1820     }
1821
1822     my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1823
1824     if (my $id = $args{'Id'}) {
1825         $oldval->Load($id);
1826     }
1827     unless ($oldval->id) { 
1828         $oldval->LoadByObjectContentAndCustomField(
1829             Object => $args{'Object'}, 
1830             Content =>  $args{'Content'}, 
1831             CustomField => $self->Id,
1832         );
1833     }
1834
1835
1836     # check to make sure we found it
1837     unless ($oldval->Id) {
1838         return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1839     }
1840
1841     # for single-value fields, we need to validate that empty string is a valid value for it
1842     if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1843         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1844     }
1845
1846     # delete it
1847
1848     my $ret = $oldval->Delete();
1849     unless ($ret) {
1850         return(0, $self->loc("Custom field value could not be found"));
1851     }
1852     return($oldval->Id, $self->loc("Custom field value deleted"));
1853 }
1854
1855
1856 =head2 ValuesForObject OBJECT
1857
1858 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT 
1859
1860 =cut
1861
1862 sub ValuesForObject {
1863     my $self = shift;
1864     my $object = shift;
1865
1866     my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
1867     unless ($self->id and $self->CurrentUserHasRight('SeeCustomField')) {
1868         # Return an empty object if they have no rights to see
1869         $values->Limit( FIELD => "id", VALUE => 0, SUBCLAUSE => "ACL" );
1870         return ($values);
1871     }
1872
1873     $values->LimitToCustomField($self->Id);
1874     $values->LimitToObject($object);
1875
1876     return ($values);
1877 }
1878
1879
1880 =head2 RegisterLookupType LOOKUPTYPE FRIENDLYNAME
1881
1882 Tell RT that a certain object accepts custom fields via a lookup type and
1883 provide a friendly name for such CFs.
1884
1885 Examples:
1886
1887     'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
1888     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
1889     'RT::User'                             => "Users",                  # loc
1890     'RT::Group'                            => "Groups",                 # loc
1891     'RT::Queue'                            => "Queues",                 # loc
1892
1893 This is a class method. 
1894
1895 =cut
1896
1897 sub RegisterLookupType {
1898     my $self = shift;
1899     my $path = shift;
1900     my $friendly_name = shift;
1901
1902     $FRIENDLY_LOOKUP_TYPES{$path} = $friendly_name;
1903 }
1904
1905 sub _ForObjectType {
1906     RT->Deprecated(
1907         Instead => 'RegisterLookupType',
1908         Remove  => '4.4',
1909     );
1910     my $self = shift;
1911     $self->RegisterLookupType(@_);
1912 }
1913
1914
1915 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1916
1917 Gets or sets the  C<IncludeContentForValue> for this custom field. RT
1918 uses this field to automatically include content into the user's browser
1919 as they display records with custom fields in RT.
1920
1921 =cut
1922
1923 sub SetIncludeContentForValue {
1924     shift->IncludeContentForValue(@_);
1925 }
1926 sub IncludeContentForValue{
1927     my $self = shift;
1928     $self->_URLTemplate('IncludeContentForValue', @_);
1929 }
1930
1931
1932
1933 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1934
1935 Gets or sets the  C<LinkValueTo> for this custom field. RT
1936 uses this field to make custom field values into hyperlinks in the user's
1937 browser as they display records with custom fields in RT.
1938
1939 =cut
1940
1941
1942 sub SetLinkValueTo {
1943     shift->LinkValueTo(@_);
1944 }
1945
1946 sub LinkValueTo {
1947     my $self = shift;
1948     $self->_URLTemplate('LinkValueTo', @_);
1949
1950 }
1951
1952
1953 =head2 _URLTemplate  NAME [VALUE]
1954
1955 With one argument, returns the _URLTemplate named C<NAME>, but only if
1956 the current user has the right to see this custom field.
1957
1958 With two arguments, attemptes to set the relevant template value.
1959
1960 =cut
1961
1962 sub _URLTemplate {
1963     my $self          = shift;
1964     my $template_name = shift;
1965     if (@_) {
1966
1967         my $value = shift;
1968         unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1969             return ( 0, $self->loc('Permission Denied') );
1970         }
1971         if (length $value and defined $value) {
1972             $self->SetAttribute( Name => $template_name, Content => $value );
1973         } else {
1974             $self->DeleteAttribute( $template_name );
1975         }
1976         return ( 1, $self->loc('Updated') );
1977     } else {
1978         unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1979             return (undef);
1980         }
1981
1982         my ($attr) = $self->Attributes->Named($template_name);
1983         return undef unless $attr;
1984         return $attr->Content;
1985     }
1986 }
1987
1988 sub SetBasedOn {
1989     my $self = shift;
1990     my $value = shift;
1991
1992     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1993         unless defined $value and length $value;
1994
1995     my $cf = RT::CustomField->new( $self->CurrentUser );
1996     $cf->SetContextObject( $self->ContextObject );
1997     $cf->Load( ref $value ? $value->id : $value );
1998
1999     return (0, "Permission Denied")
2000         unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
2001
2002     # XXX: Remove this restriction once we support lists and cascaded selects
2003     if ( $self->RenderType =~ /List/ ) {
2004         return (0, $self->loc("We can't currently render as a List when basing categories on another custom field.  Please use another render type."));
2005     }
2006
2007     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
2008 }
2009
2010 sub BasedOnObj {
2011     my $self = shift;
2012
2013     my $obj = RT::CustomField->new( $self->CurrentUser );
2014     $obj->SetContextObject( $self->ContextObject );
2015     if ( $self->BasedOn ) {
2016         $obj->Load( $self->BasedOn );
2017     }
2018     return $obj;
2019 }
2020
2021
2022
2023
2024
2025
2026 =head2 id
2027
2028 Returns the current value of id. 
2029 (In the database, id is stored as int(11).)
2030
2031
2032 =cut
2033
2034
2035 =head2 Name
2036
2037 Returns the current value of Name. 
2038 (In the database, Name is stored as varchar(200).)
2039
2040
2041
2042 =head2 SetName VALUE
2043
2044
2045 Set Name to VALUE. 
2046 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2047 (In the database, Name will be stored as a varchar(200).)
2048
2049
2050 =cut
2051
2052
2053 =head2 Type
2054
2055 Returns the current value of Type. 
2056 (In the database, Type is stored as varchar(200).)
2057
2058
2059
2060 =head2 SetType VALUE
2061
2062
2063 Set Type to VALUE. 
2064 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2065 (In the database, Type will be stored as a varchar(200).)
2066
2067
2068 =cut
2069
2070
2071 =head2 RenderType
2072
2073 Returns the current value of RenderType. 
2074 (In the database, RenderType is stored as varchar(64).)
2075
2076
2077
2078 =head2 SetRenderType VALUE
2079
2080
2081 Set RenderType to VALUE. 
2082 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2083 (In the database, RenderType will be stored as a varchar(64).)
2084
2085
2086 =cut
2087
2088
2089 =head2 MaxValues
2090
2091 Returns the current value of MaxValues. 
2092 (In the database, MaxValues is stored as int(11).)
2093
2094
2095
2096 =head2 SetMaxValues VALUE
2097
2098
2099 Set MaxValues to VALUE. 
2100 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2101 (In the database, MaxValues will be stored as a int(11).)
2102
2103
2104 =cut
2105
2106
2107 =head2 Pattern
2108
2109 Returns the current value of Pattern. 
2110 (In the database, Pattern is stored as text.)
2111
2112
2113
2114 =head2 SetPattern VALUE
2115
2116
2117 Set Pattern to VALUE. 
2118 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2119 (In the database, Pattern will be stored as a text.)
2120
2121
2122 =cut
2123
2124
2125 =head2 BasedOn
2126
2127 Returns the current value of BasedOn. 
2128 (In the database, BasedOn is stored as int(11).)
2129
2130
2131
2132 =head2 SetBasedOn VALUE
2133
2134
2135 Set BasedOn to VALUE. 
2136 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2137 (In the database, BasedOn will be stored as a int(11).)
2138
2139
2140 =cut
2141
2142
2143 =head2 Description
2144
2145 Returns the current value of Description. 
2146 (In the database, Description is stored as varchar(255).)
2147
2148
2149
2150 =head2 SetDescription VALUE
2151
2152
2153 Set Description to VALUE. 
2154 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2155 (In the database, Description will be stored as a varchar(255).)
2156
2157
2158 =cut
2159
2160
2161 =head2 SortOrder
2162
2163 Returns the current value of SortOrder. 
2164 (In the database, SortOrder is stored as int(11).)
2165
2166
2167
2168 =head2 SetSortOrder VALUE
2169
2170
2171 Set SortOrder to VALUE. 
2172 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2173 (In the database, SortOrder will be stored as a int(11).)
2174
2175
2176 =cut
2177
2178
2179 =head2 LookupType
2180
2181 Returns the current value of LookupType. 
2182 (In the database, LookupType is stored as varchar(255).)
2183
2184
2185
2186 =head2 SetLookupType VALUE
2187
2188
2189 Set LookupType to VALUE. 
2190 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2191 (In the database, LookupType will be stored as a varchar(255).)
2192
2193
2194 =cut
2195
2196
2197 =head2 Creator
2198
2199 Returns the current value of Creator. 
2200 (In the database, Creator is stored as int(11).)
2201
2202
2203 =cut
2204
2205
2206 =head2 Created
2207
2208 Returns the current value of Created. 
2209 (In the database, Created is stored as datetime.)
2210
2211
2212 =cut
2213
2214
2215 =head2 LastUpdatedBy
2216
2217 Returns the current value of LastUpdatedBy. 
2218 (In the database, LastUpdatedBy is stored as int(11).)
2219
2220
2221 =cut
2222
2223
2224 =head2 LastUpdated
2225
2226 Returns the current value of LastUpdated. 
2227 (In the database, LastUpdated is stored as datetime.)
2228
2229
2230 =cut
2231
2232
2233 =head2 Disabled
2234
2235 Returns the current value of Disabled. 
2236 (In the database, Disabled is stored as smallint(6).)
2237
2238
2239
2240 =head2 SetDisabled VALUE
2241
2242
2243 Set Disabled to VALUE. 
2244 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2245 (In the database, Disabled will be stored as a smallint(6).)
2246
2247
2248 =cut
2249
2250
2251
2252 sub _CoreAccessible {
2253     {
2254      
2255         id =>
2256         {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2257         Name => 
2258         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2259         Type => 
2260         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2261         RenderType => 
2262         {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2263         MaxValues => 
2264         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2265         Pattern => 
2266         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2267         ValuesClass => 
2268         {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2269         BasedOn => 
2270         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2271         Description => 
2272         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2273         SortOrder => 
2274         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2275         LookupType => 
2276         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2277         Creator => 
2278         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2279         Created => 
2280         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2281         LastUpdatedBy => 
2282         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2283         LastUpdated => 
2284         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2285         Disabled => 
2286         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2287
2288  }
2289 };
2290
2291 sub FindDependencies {
2292     my $self = shift;
2293     my ($walker, $deps) = @_;
2294
2295     $self->SUPER::FindDependencies($walker, $deps);
2296
2297     $deps->Add( out => $self->BasedOnObj )
2298         if $self->BasedOnObj->id;
2299
2300     my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
2301     $applied->LimitToCustomField( $self->id );
2302     $deps->Add( in => $applied );
2303
2304     $deps->Add( in => $self->Values ) if $self->ValuesClass eq "RT::CustomFieldValues";
2305 }
2306
2307
2308 RT::Base->_ImportOverlays();
2309
2310 1;