1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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.
30 # CONTRIBUTION SUBMISSION POLICY:
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.)
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.
47 # END BPS TAGGED BLOCK }}}
49 package RT::CustomField;
56 use base 'RT::Record';
58 sub Table {'CustomFields'}
61 use RT::CustomFieldValues;
62 use RT::ObjectCustomFields;
63 use RT::ObjectCustomFieldValues;
70 labels => [ 'Select multiple values', # loc
71 'Select one value', # loc
72 'Select up to [_1] values', # loc
78 # Default is the first one
82 single => [ 'Select box', # loc
93 labels => [ 'Enter multiple values', # loc
94 'Enter one value', # loc
95 'Enter up to [_1] values', # loc
102 'Fill in multiple text areas', # loc
103 'Fill in one text area', # loc
104 'Fill in up to [_1] text areas', # loc
111 'Fill in multiple wikitext areas', # loc
112 'Fill in one wikitext area', # loc
113 'Fill in up to [_1] wikitext areas', # loc
121 'Upload multiple images', # loc
122 'Upload one image', # loc
123 'Upload up to [_1] images', # loc
130 'Upload multiple files', # loc
131 'Upload one file', # loc
132 'Upload up to [_1] files', # loc
140 'Combobox: Select or enter multiple values', # loc
141 'Combobox: Select or enter one value', # loc
142 'Combobox: Select or enter up to [_1] values', # loc
149 'Enter multiple values with autocompletion', # loc
150 'Enter one value with autocompletion', # loc
151 'Enter up to [_1] values with autocompletion', # loc
159 'Select multiple dates', # loc
161 'Select up to [_1] dates', # loc
168 'Select multiple datetimes', # loc
169 'Select datetime', # loc
170 'Select up to [_1] datetimes', # loc
174 'Enter multiple time values (UNSUPPORTED)',
175 'Enter a time value',
176 'Enter [_1] time values (UNSUPPORTED)',
183 labels => [ 'Enter multiple IP addresses', # loc
184 'Enter one IP address', # loc
185 'Enter up to [_1] IP addresses', # loc
192 labels => [ 'Enter multiple IP address ranges', # loc
193 'Enter one IP address range', # loc
194 'Enter up to [_1] IP address ranges', # loc
200 our %FRIENDLY_OBJECT_TYPES = ();
202 RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket' => "Tickets", ); #loc
203 RT::CustomField->_ForObjectType(
204 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", ); #loc
205 RT::CustomField->_ForObjectType( 'RT::User' => "Users", ); #loc
206 RT::CustomField->_ForObjectType( 'RT::Queue' => "Queues", ); #loc
207 RT::CustomField->_ForObjectType( 'RT::Group' => "Groups", ); #loc
210 SeeCustomField => 'View custom fields', # loc_pair
211 AdminCustomField => 'Create, modify and delete custom fields', # loc_pair
212 AdminCustomFieldValues => 'Create, modify and delete custom fields values', # loc_pair
213 ModifyCustomField => 'Add, modify and delete custom field values for objects' # loc_pair
216 our $RIGHT_CATEGORIES = {
217 SeeCustomField => 'General',
218 AdminCustomField => 'Admin',
219 AdminCustomFieldValues => 'Admin',
220 ModifyCustomField => 'Staff',
223 # Tell RT::ACE that this sort of object can get acls granted
224 $RT::ACE::OBJECT_TYPES{'RT::CustomField'} = 1;
226 __PACKAGE__->AddRights(%$RIGHTS);
227 __PACKAGE__->AddRightCategories(%$RIGHT_CATEGORIES);
229 =head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...]
231 Adds the given rights to the list of possible rights. This method
232 should be called during server startup, not at runtime.
239 $RIGHTS = { %$RIGHTS, %new };
240 %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES,
241 map { lc($_) => $_ } keys %new);
244 sub AvailableRights {
249 =head2 RightCategories
251 Returns a hashref where the keys are rights for this type of object and the
252 values are the category (General, Staff, Admin) the right falls into.
256 sub RightCategories {
257 return $RIGHT_CATEGORIES;
260 =head2 AddRightCategories C<RIGHT>, C<CATEGORY> [, ...]
262 Adds the given right and category pairs to the list of right categories. This
263 method should be called during server startup, not at runtime.
267 sub AddRightCategories {
268 my $self = shift if ref $_[0] or $_[0] eq __PACKAGE__;
270 $RIGHT_CATEGORIES = { %$RIGHT_CATEGORIES, %new };
275 RT::CustomField_Overlay - overlay for RT::CustomField
279 =head1 'CORE' METHODS
281 =head2 Create PARAMHASH
283 Create takes a hash of values and creates a row in the database:
288 varchar(255) 'Pattern'.
289 smallint(6) 'Repeated'.
290 varchar(255) 'Description'.
292 varchar(255) 'LookupType'.
293 smallint(6) 'Disabled'.
295 C<LookupType> is generally the result of either
296 C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType>.
312 IncludeContentForValue => '',
316 unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) {
317 return (0, $self->loc('Permission Denied'));
320 if ( $args{TypeComposite} ) {
321 @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
323 elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) {
324 # old style Type string
325 $args{'MaxValues'} = $1 ? 1 : 0;
327 $args{'MaxValues'} = int $args{'MaxValues'};
329 if ( !exists $args{'Queue'}) {
330 # do nothing -- things below are strictly backward compat
332 elsif ( ! $args{'Queue'} ) {
333 unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
334 return ( 0, $self->loc('Permission Denied') );
336 $args{'LookupType'} = 'RT::Queue-RT::Ticket';
339 my $queue = RT::Queue->new($self->CurrentUser);
340 $queue->Load($args{'Queue'});
341 unless ($queue->Id) {
342 return (0, $self->loc("Queue not found"));
344 unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
345 return ( 0, $self->loc('Permission Denied') );
347 $args{'LookupType'} = 'RT::Queue-RT::Ticket';
348 $args{'Queue'} = $queue->Id;
351 my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} );
352 return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok;
354 if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) {
355 $RT::Logger->debug("Support for 'multiple' Texts or Comboboxes is not implemented");
356 $args{'MaxValues'} = 1;
359 if ( $args{'RenderType'} ||= undef ) {
360 my $composite = join '-', @args{'Type', 'MaxValues'};
361 return (0, $self->loc("This custom field has no Render Types"))
362 unless $self->HasRenderTypes( $composite );
364 if ( $args{'RenderType'} eq $self->DefaultRenderType( $composite ) ) {
365 $args{'RenderType'} = undef;
367 return (0, $self->loc("Invalid Render Type") )
368 unless grep $_ eq $args{'RenderType'}, $self->RenderTypes( $composite );
372 $args{'ValuesClass'} = undef if ($args{'ValuesClass'} || '') eq 'RT::CustomFieldValues';
373 if ( $args{'ValuesClass'} ||= undef ) {
374 return (0, $self->loc("This Custom Field can not have list of values"))
375 unless $self->IsSelectionType( $args{'Type'} );
377 unless ( $self->ValidateValuesClass( $args{'ValuesClass'} ) ) {
378 return (0, $self->loc("Invalid Custom Field values source"));
382 (my $rv, $msg) = $self->SUPER::Create(
383 Name => $args{'Name'},
384 Type => $args{'Type'},
385 RenderType => $args{'RenderType'},
386 MaxValues => $args{'MaxValues'},
387 Pattern => $args{'Pattern'},
388 BasedOn => $args{'BasedOn'},
389 ValuesClass => $args{'ValuesClass'},
390 Description => $args{'Description'},
391 Disabled => $args{'Disabled'},
392 LookupType => $args{'LookupType'},
393 Repeated => $args{'Repeated'},
397 if ( exists $args{'LinkValueTo'}) {
398 $self->SetLinkValueTo($args{'LinkValueTo'});
401 if ( exists $args{'IncludeContentForValue'}) {
402 $self->SetIncludeContentForValue($args{'IncludeContentForValue'});
405 if ( exists $args{'UILocation'} ) {
406 $self->SetUILocation( $args{'UILocation'} );
409 return ($rv, $msg) unless exists $args{'Queue'};
411 # Compat code -- create a new ObjectCustomField mapping
412 my $OCF = RT::ObjectCustomField->new( $self->CurrentUser );
414 CustomField => $self->Id,
415 ObjectId => $args{'Queue'},
424 Load a custom field. If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
430 my $id = shift || '';
432 if ( $id =~ /^\d+$/ ) {
433 return $self->SUPER::Load( $id );
435 return $self->LoadByName( Name => $id );
441 =head2 LoadByName (Queue => QUEUEID, Name => NAME)
443 Loads the Custom field named NAME.
445 Will load a Disabled Custom Field even if there is a non-disabled Custom Field
448 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
450 If the Queue parameter is '0', look for global ticket custom fields.
452 If no queue parameter is specified, look for any and all custom fields with this name.
454 BUG/TODO, this won't let you specify that you only want user or group CFs.
458 # Compatibility for API change after 3.0 beta 1
459 *LoadNameAndQueue = \&LoadByName;
460 # Change after 3.4 beta.
461 *LoadByNameAndQueue = \&LoadByName;
471 unless ( defined $args{'Name'} && length $args{'Name'} ) {
472 $RT::Logger->error("Couldn't load Custom Field without Name");
473 return wantarray ? (0, $self->loc("No name provided")) : 0;
476 # if we're looking for a queue by name, make it a number
477 if ( defined $args{'Queue'} && ($args{'Queue'} =~ /\D/ || !$self->ContextObject) ) {
478 my $QueueObj = RT::Queue->new( $self->CurrentUser );
479 $QueueObj->Load( $args{'Queue'} );
480 $args{'Queue'} = $QueueObj->Id;
481 $self->SetContextObject( $QueueObj )
482 unless $self->ContextObject;
485 # XXX - really naive implementation. Slow. - not really. still just one query
487 my $CFs = RT::CustomFields->new( $self->CurrentUser );
488 $CFs->SetContextObject( $self->ContextObject );
489 my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
490 $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
491 # Don't limit to queue if queue is 0. Trying to do so breaks
492 # RT::Group type CFs.
493 if ( defined $args{'Queue'} ) {
494 $CFs->LimitToQueue( $args{'Queue'} );
497 # When loading by name, we _can_ load disabled fields, but prefer
498 # non-disabled fields.
501 { FIELD => "Disabled", ORDER => 'ASC' },
504 # We only want one entry.
505 $CFs->RowsPerPage(1);
507 # version before 3.8 just returns 0, so we need to test if wantarray to be
508 # backward compatible.
509 return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
511 return $self->LoadById( $first->id );
517 =head2 Custom field values
521 Return a object (collection) of all acceptable values for this Custom Field.
522 Class of the object can vary and depends on the return value
523 of the C<ValuesClass> method.
527 *ValuesObj = \&Values;
532 my $class = $self->ValuesClass;
533 if ( $class ne 'RT::CustomFieldValues') {
534 eval "require $class" or die "$@";
536 my $cf_values = $class->new( $self->CurrentUser );
537 # if the user has no rights, return an empty object
538 if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
539 $cf_values->LimitToCustomField( $self->Id );
541 $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
549 Create a new value for this CustomField. Takes a paramhash containing the elements Name, Description and SortOrder
557 unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) {
558 return (0, $self->loc('Permission Denied'));
562 if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
563 return (0, $self->loc("Can't add a custom field value without a name"));
566 my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
567 return $newval->Create( %args, CustomField => $self->Id );
573 =head3 DeleteValue ID
575 Deletes a value from this custom field by id.
577 Does not remove this value for any article which has had it selected
584 unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) {
585 return (0, $self->loc('Permission Denied'));
588 my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
589 $val_to_del->Load( $id );
590 unless ( $val_to_del->Id ) {
591 return (0, $self->loc("Couldn't find that value"));
593 unless ( $val_to_del->CustomField == $self->Id ) {
594 return (0, $self->loc("That is not a value for this custom field"));
597 my $retval = $val_to_del->Delete;
599 return (0, $self->loc("Custom field value could not be deleted"));
601 return ($retval, $self->loc("Custom field value deleted"));
605 =head2 ValidateQueue Queue
607 Make sure that the name specified is valid
615 return 0 unless length $value;
617 return $self->SUPER::ValidateName($value);
620 =head2 ValidateQueue Queue
622 Make sure that the queue specified is a valid queue name
630 return undef unless defined $id;
631 # 0 means "Global" null would _not_ be ok.
632 return 1 if $id eq '0';
634 my $q = RT::Queue->new( RT->SystemUser );
636 return undef unless $q->id;
644 Retuns an array of the types of CustomField that are supported
649 return (sort {(($FieldTypes{$a}{sort_order}||999) <=> ($FieldTypes{$b}{sort_order}||999)) or ($a cmp $b)} keys %FieldTypes);
653 =head2 IsSelectionType
655 Retuns a boolean value indicating whether the C<Values> method makes sense
656 to this Custom Field.
660 sub IsSelectionType {
662 my $type = @_? shift : $self->Type;
663 return undef unless $type;
664 return $FieldTypes{$type}->{selection_type};
669 =head2 IsExternalValues
673 sub IsExternalValues {
675 return 0 unless $self->IsSelectionType( @_ );
676 return $self->ValuesClass eq 'RT::CustomFieldValues'? 0 : 1;
681 return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues';
686 my $class = shift || 'RT::CustomFieldValues';
688 if ( $class eq 'RT::CustomFieldValues' ) {
689 return $self->_Set( Field => 'ValuesClass', Value => undef, @_ );
692 return (0, $self->loc("This Custom Field can not have list of values"))
693 unless $self->IsSelectionType;
695 unless ( $self->ValidateValuesClass( $class ) ) {
696 return (0, $self->loc("Invalid Custom Field values source"));
698 return $self->_Set( Field => 'ValuesClass', Value => $class, @_ );
701 sub ValidateValuesClass {
705 return 1 if !defined $class || $class eq 'RT::CustomFieldValues';
706 return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesSources');
711 =head2 FriendlyType [TYPE, MAX_VALUES]
713 Returns a localized human-readable version of the custom field type.
714 If a custom field type is specified as the parameter, the friendly type for that type will be returned
721 my $type = @_ ? shift : $self->Type;
722 my $max = @_ ? shift : $self->MaxValues;
723 $max = 0 unless $max;
725 if (my $friendly_type = $FieldTypes{$type}->{labels}->[$max>2 ? 2 : $max]) {
726 return ( $self->loc( $friendly_type, $max ) );
729 return ( $self->loc( $type ) );
733 sub FriendlyTypeComposite {
735 my $composite = shift || $self->TypeComposite;
736 return $self->FriendlyType(split(/-/, $composite, 2));
740 =head2 ValidateType TYPE
742 Takes a single string. returns true if that string is a value
752 if ( $type =~ s/(?:Single|Multiple)$// ) {
753 $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
756 if ( $FieldTypes{$type} ) {
768 if ($type =~ s/(?:(Single)|Multiple)$//) {
769 $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
770 $self->SetMaxValues($1 ? 1 : 0);
772 $self->_Set(Field => 'Type', Value =>$type);
775 =head2 SetPattern STRING
777 Takes a single string representing a regular expression. Performs basic
778 validation on that regex, and sets the C<Pattern> field for the CF if it
787 my ($ok, $msg) = $self->_IsValidRegex($regex);
789 return $self->_Set(Field => 'Pattern', Value => $regex);
792 return (0, $self->loc("Invalid pattern: [_1]", $msg));
796 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
798 Tests if the string contains an invalid regex.
804 my $regex = shift or return (1, 'valid');
807 local $SIG{__DIE__} = sub { 1 };
808 local $SIG{__WARN__} = sub { 1 };
810 if (eval { qr/$regex/; 1 }) {
815 $err =~ s{[,;].*}{}; # strip debug info from error
823 Returns true if this CustomField only accepts a single value.
824 Returns false if it accepts multiple values
830 if (($self->MaxValues||0) == 1) {
838 sub UnlimitedValues {
840 if (($self->MaxValues||0) == 0) {
849 =head2 CurrentUserHasRight RIGHT
851 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
855 sub CurrentUserHasRight {
859 return $self->CurrentUser->HasRight(
865 =head2 ACLEquivalenceObjects
867 Returns list of objects via which users can get rights on this custom field. For custom fields
868 these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
872 sub ACLEquivalenceObjects {
875 my $ctx = $self->ContextObject
877 return ($ctx, $ctx->ACLEquivalenceObjects);
880 =head2 ContextObject and SetContextObject
882 Set or get a context for this object. It can be ticket, queue or another object
883 this CF applies to. Used for ACL control, for example SeeCustomField can be granted on
884 queue level to allow people to see all fields applied to the queue.
888 sub SetContextObject {
890 return $self->{'context_object'} = shift;
895 return $self->{'context_object'};
898 sub ValidContextType {
903 $valid{$_}++ for split '-', $self->LookupType;
904 delete $valid{'RT::Transaction'};
906 return $valid{$class};
909 =head2 LoadContextObject
911 Takes an Id for a Context Object and loads the right kind of RT::Object
912 for this particular Custom Field (based on the LookupType) and returns it.
913 This is a good way to ensure you don't try to use a Queue as a Context
914 Object on a User Custom Field.
918 sub LoadContextObject {
921 my $contextid = shift;
923 unless ( $self->ValidContextType($type) ) {
924 RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id);
928 my $context_object = $type->new( $self->CurrentUser );
929 my ($id, $msg) = $context_object->LoadById( $contextid );
931 RT->Logger->debug("Invalid ContextObject id: $msg");
934 return $context_object;
937 =head2 ValidateContextObject
939 Ensure that a given ContextObject applies to this Custom Field.
940 For custom fields that are assigned to Queues or to Classes, this checks that the Custom
941 Field is actually applied to that objects. For Global Custom Fields, it returns true
942 as long as the Object is of the right type, because you may be using
943 your permissions on a given Queue of Class to see a Global CF.
944 For CFs that are only applied Globally, you don't need a ContextObject.
948 sub ValidateContextObject {
952 return 1 if $self->IsApplied(0);
954 # global only custom fields don't have objects
955 # that should be used as context objects.
956 return if $self->ApplyGlobally;
958 # Otherwise, make sure we weren't passed a user object that we're
959 # supposed to treat as a queue.
960 return unless $self->ValidContextType(ref $object);
962 # Check that it is applied correctly
963 my ($applied_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects);
964 return unless $applied_to;
965 return $self->IsApplied($applied_to->id);
972 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
973 return ( 0, $self->loc('Permission Denied') );
975 return $self->SUPER::_Set( @_ );
983 Takes the name of a table column.
984 Returns its value as a string, if the user passes an ACL check
990 return undef unless $self->id;
992 # we need to do the rights check
993 unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
995 "Permission denied. User #". $self->CurrentUser->id
996 ." has no SeeCustomField right on CF #". $self->id
1000 return $self->__Value( @_ );
1007 1 will cause this custom field to no longer be avaialble for objects.
1008 0 will re-enable this field.
1013 =head2 SetTypeComposite
1015 Set this custom field's type and maximum values as a composite value
1019 sub SetTypeComposite {
1021 my $composite = shift;
1023 my $old = $self->TypeComposite;
1025 my ($type, $max_values) = split(/-/, $composite, 2);
1026 if ( $type ne $self->Type ) {
1027 my ($status, $msg) = $self->SetType( $type );
1028 return ($status, $msg) unless $status;
1030 if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
1031 my ($status, $msg) = $self->SetMaxValues( $max_values );
1032 return ($status, $msg) unless $status;
1034 my $render = $self->RenderType;
1035 if ( $render and not grep { $_ eq $render } $self->RenderTypes ) {
1036 # We switched types and our render type is no longer valid, so unset it
1037 # and use the default
1038 $self->SetRenderType( undef );
1040 return 1, $self->loc(
1041 "Type changed from '[_1]' to '[_2]'",
1042 $self->FriendlyTypeComposite( $old ),
1043 $self->FriendlyTypeComposite( $composite ),
1047 =head2 TypeComposite
1049 Returns a composite value composed of this object's type and maximum values
1056 return join '-', ($self->Type || ''), ($self->MaxValues || 0);
1059 =head2 TypeComposites
1061 Returns an array of all possible composite values for custom fields.
1065 sub TypeComposites {
1067 return grep !/(?:[Tt]ext|Combobox|Date|DateTime|TimeValue)-0/, map { ("$_-1", "$_-0") } $self->Types;
1072 Returns the type of form widget to render for this custom field. Currently
1073 this only affects fields which return true for L</HasRenderTypes>.
1079 return '' unless $self->HasRenderTypes;
1081 return $self->_Value( 'RenderType', @_ )
1082 || $self->DefaultRenderType;
1085 =head2 SetRenderType TYPE
1087 Sets this custom field's render type.
1094 return (0, $self->loc("This custom field has no Render Types"))
1095 unless $self->HasRenderTypes;
1097 if ( !$type || $type eq $self->DefaultRenderType ) {
1098 return $self->_Set( Field => 'RenderType', Value => undef, @_ );
1101 if ( not grep { $_ eq $type } $self->RenderTypes ) {
1102 return (0, $self->loc("Invalid Render Type for custom field of type [_1]",
1103 $self->FriendlyType));
1106 # XXX: Remove this restriction once we support lists and cascaded selects
1107 if ( $self->BasedOnObj->id and $type =~ /List/ ) {
1108 return (0, $self->loc("We can't currently render as a List when basing categories on another custom field. Please use another render type."));
1111 return $self->_Set( Field => 'RenderType', Value => $type, @_ );
1114 =head2 DefaultRenderType [TYPE COMPOSITE]
1116 Returns the default render type for this custom field's type or the TYPE
1117 COMPOSITE specified as an argument.
1121 sub DefaultRenderType {
1123 my $composite = @_ ? shift : $self->TypeComposite;
1124 my ($type, $max) = split /-/, $composite, 2;
1125 return unless $type and $self->HasRenderTypes($composite);
1126 return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0];
1129 =head2 HasRenderTypes [TYPE_COMPOSITE]
1131 Returns a boolean value indicating whether the L</RenderTypes> and
1132 L</RenderType> methods make sense for this custom field.
1134 Currently true only for type C<Select>.
1138 sub HasRenderTypes {
1140 my ($type, $max) = split /-/, (@_ ? shift : $self->TypeComposite), 2;
1141 return undef unless $type;
1142 return defined $FieldTypes{$type}->{render_types}
1143 ->{ $max == 1 ? 'single' : 'multiple' };
1146 =head2 RenderTypes [TYPE COMPOSITE]
1148 Returns the valid render types for this custom field's type or the TYPE
1149 COMPOSITE specified as an argument.
1155 my $composite = @_ ? shift : $self->TypeComposite;
1156 my ($type, $max) = split /-/, $composite, 2;
1157 return unless $type and $self->HasRenderTypes($composite);
1158 return @{$FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }};
1161 =head2 SetLookupType
1163 Autrijus: care to doc how LookupTypes work?
1170 if ( $lookup ne $self->LookupType ) {
1171 # Okay... We need to invalidate our existing relationships
1172 my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
1173 $ObjectCustomFields->LimitToCustomField($self->Id);
1174 $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
1176 return $self->_Set(Field => 'LookupType', Value =>$lookup);
1181 Returns an array of LookupTypes available
1188 return keys %FRIENDLY_OBJECT_TYPES;
1191 my @FriendlyObjectTypes = (
1192 "[_1] objects", # loc
1193 "[_1]'s [_2] objects", # loc
1194 "[_1]'s [_2]'s [_3] objects", # loc
1197 =head2 FriendlyLookupType
1199 Returns a localized description of the type of this custom field
1203 sub FriendlyLookupType {
1205 my $lookup = shift || $self->LookupType;
1207 return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
1208 if (defined $FRIENDLY_OBJECT_TYPES{$lookup} );
1210 my @types = map { s/^RT::// ? $self->loc($_) : $_ }
1211 grep { defined and length }
1212 split( /-/, $lookup )
1214 return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
1217 sub RecordClassFromLookupType {
1219 my ($class) = ($self->LookupType =~ /^([^-]+)/);
1222 "Custom Field #". $self->id
1223 ." has incorrect LookupType '". $self->LookupType ."'"
1230 sub CollectionClassFromLookupType {
1233 my $record_class = $self->RecordClassFromLookupType;
1234 return undef unless $record_class;
1236 my $collection_class;
1237 if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
1238 $collection_class = $record_class.'Collection';
1239 } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
1240 $collection_class = $record_class.'es';
1241 } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
1242 $collection_class = $record_class.'s';
1244 $RT::Logger->error("Can not find a collection class for record class '$record_class'");
1247 return $collection_class;
1250 =head1 ApplyGlobally
1252 Certain custom fields (users, groups) should only be applied globally
1253 but rather than regexing in code for LookupType =~ RT::Queue, we'll codify
1261 return ($self->LookupType =~ /^RT::(?:Group|User)/io);
1267 Returns collection with objects this custom field is applied to.
1268 Class of the collection depends on L</LookupType>.
1269 See all L</NotAppliedTo> .
1271 Doesn't takes into account if object is applied globally.
1278 my ($res, $ocfs_alias) = $self->_AppliedTo;
1279 return $res unless $res;
1282 ALIAS => $ocfs_alias,
1284 OPERATOR => 'IS NOT',
1293 Returns collection with objects this custom field is not applied to.
1294 Class of the collection depends on L</LookupType>.
1295 See all L</AppliedTo> .
1297 Doesn't takes into account if object is applied globally.
1304 my ($res, $ocfs_alias) = $self->_AppliedTo;
1305 return $res unless $res;
1308 ALIAS => $ocfs_alias,
1320 my ($class) = $self->CollectionClassFromLookupType;
1321 return undef unless $class;
1323 my $res = $class->new( $self->CurrentUser );
1325 # If CF is a Group CF, only display user-defined groups
1326 if ( $class eq 'RT::Groups' ) {
1327 $res->LimitToUserDefinedGroups;
1330 $res->OrderBy( FIELD => 'Name' );
1331 my $ocfs_alias = $res->Join(
1335 TABLE2 => 'ObjectCustomFields',
1336 FIELD2 => 'ObjectId',
1339 LEFTJOIN => $ocfs_alias,
1340 ALIAS => $ocfs_alias,
1341 FIELD => 'CustomField',
1344 return ($res, $ocfs_alias);
1349 Takes object id and returns corresponding L<RT::ObjectCustomField>
1350 record if this custom field is applied to the object. Use 0 to check
1351 if custom field is applied globally.
1358 my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1359 $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1360 return undef unless $ocf->id;
1364 =head2 AddToObject OBJECT
1366 Add this custom field as a custom field for a single object, such as a queue or group.
1376 my $id = $object->Id || 0;
1378 unless (index($self->LookupType, ref($object)) == 0) {
1379 return ( 0, $self->loc('Lookup type mismatch') );
1382 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1383 return ( 0, $self->loc('Permission Denied') );
1386 if ( $self->IsApplied( $id ) ) {
1387 return ( 0, $self->loc("Custom field is already applied to the object") );
1392 return (0, $self->loc("Couldn't apply custom field to an object as it's global already") )
1393 if $self->IsApplied( 0 );
1396 my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
1397 $applied->LimitToCustomField( $self->id );
1398 while ( my $record = $applied->Next ) {
1403 my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1404 my ( $oid, $msg ) = $ocf->Create(
1405 ObjectId => $id, CustomField => $self->id,
1407 return ( $oid, $msg );
1411 =head2 RemoveFromObject OBJECT
1413 Remove this custom field for a single object, such as a queue or group.
1419 sub RemoveFromObject {
1422 my $id = $object->Id || 0;
1424 unless (index($self->LookupType, ref($object)) == 0) {
1425 return ( 0, $self->loc('Object type mismatch') );
1428 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1429 return ( 0, $self->loc('Permission Denied') );
1432 my $ocf = $self->IsApplied( $id );
1434 return ( 0, $self->loc("This custom field does not apply to that object") );
1437 # XXX: Delete doesn't return anything
1438 my ( $oid, $msg ) = $ocf->Delete;
1439 return ( $oid, $msg );
1443 =head2 AddValueForObject HASH
1445 Adds a custom field value for a record object of some kind.
1446 Takes a param hash of
1460 sub AddValueForObject {
1465 LargeContent => undef,
1466 ContentType => undef,
1469 my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1471 unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1472 return ( 0, $self->loc('Permission Denied') );
1475 unless ( $self->MatchPattern($args{'Content'}) ) {
1476 return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1479 $RT::Handle->BeginTransaction;
1481 if ( $self->MaxValues ) {
1482 my $current_values = $self->ValuesForObject($obj);
1483 my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1485 # (The +1 is for the new value we're adding)
1487 # If we have a set of current values and we've gone over the maximum
1488 # allowed number of values, we'll need to delete some to make room.
1489 # which former values are blown away is not guaranteed
1491 while ($extra_values) {
1492 my $extra_item = $current_values->Next;
1493 unless ( $extra_item->id ) {
1494 $RT::Logger->crit( "We were just asked to delete "
1495 ."a custom field value that doesn't exist!" );
1496 $RT::Handle->Rollback();
1499 $extra_item->Delete;
1504 if (my $canonicalizer = $self->can('_CanonicalizeValue'.$self->Type)) {
1505 $canonicalizer->($self, \%args);
1510 my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1511 my ($val, $msg) = $newval->Create(
1512 ObjectType => ref($obj),
1513 ObjectId => $obj->Id,
1514 Content => $args{'Content'},
1515 LargeContent => $args{'LargeContent'},
1516 ContentType => $args{'ContentType'},
1517 CustomField => $self->Id
1521 $RT::Handle->Rollback();
1522 return ($val, $self->loc("Couldn't create record: [_1]", $msg));
1525 $RT::Handle->Commit();
1532 sub _CanonicalizeValueDateTime {
1535 my $DateObj = RT::Date->new( $self->CurrentUser );
1536 $DateObj->Set( Format => 'unknown',
1537 Value => $args->{'Content'} );
1538 $args->{'Content'} = $DateObj->ISO;
1541 # For date, we need to store Content as ISO date
1542 sub _CanonicalizeValueDate {
1546 # in case user input date with time, let's omit it by setting timezone
1547 # to utc so "hour" won't affect "day"
1548 my $DateObj = RT::Date->new( $self->CurrentUser );
1549 $DateObj->Set( Format => 'unknown',
1550 Value => $args->{'Content'},
1553 $args->{'Content'} = $DateObj->Date( Timezone => 'UTC' );
1556 =head2 MatchPattern STRING
1558 Tests the incoming string against the Pattern of this custom field object
1559 and returns a boolean; returns true if the Pattern is empty.
1565 my $regex = $self->Pattern or return 1;
1567 return (( defined $_[0] ? $_[0] : '') =~ $regex);
1573 =head2 FriendlyPattern
1575 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1580 sub FriendlyPattern {
1582 my $regex = $self->Pattern;
1584 return '' unless length $regex;
1585 if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1586 return '[' . $self->loc($1) . ']';
1596 =head2 DeleteValueForObject HASH
1598 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1600 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1604 sub DeleteValueForObject {
1606 my %args = ( Object => undef,
1612 unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1613 return (0, $self->loc('Permission Denied'));
1616 my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1618 if (my $id = $args{'Id'}) {
1621 unless ($oldval->id) {
1622 $oldval->LoadByObjectContentAndCustomField(
1623 Object => $args{'Object'},
1624 Content => $args{'Content'},
1625 CustomField => $self->Id,
1630 # check to make sure we found it
1631 unless ($oldval->Id) {
1632 return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1635 # for single-value fields, we need to validate that empty string is a valid value for it
1636 if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1637 return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1642 my $ret = $oldval->Delete();
1644 return(0, $self->loc("Custom field value could not be found"));
1646 return($oldval->Id, $self->loc("Custom field value deleted"));
1650 =head2 ValuesForObject OBJECT
1652 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT
1656 sub ValuesForObject {
1660 my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
1661 unless ($self->CurrentUserHasRight('SeeCustomField')) {
1662 # Return an empty object if they have no rights to see
1667 $values->LimitToCustomField($self->Id);
1668 $values->LimitToEnabled();
1669 $values->LimitToObject($object);
1675 =head2 _ForObjectType PATH FRIENDLYNAME
1677 Tell RT that a certain object accepts custom fields
1681 'RT::Queue-RT::Ticket' => "Tickets", # loc
1682 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc
1683 'RT::User' => "Users", # loc
1684 'RT::Group' => "Groups", # loc
1686 This is a class method.
1690 sub _ForObjectType {
1693 my $friendly_name = shift;
1695 $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1700 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1702 Gets or sets the C<IncludeContentForValue> for this custom field. RT
1703 uses this field to automatically include content into the user's browser
1704 as they display records with custom fields in RT.
1708 sub SetIncludeContentForValue {
1709 shift->IncludeContentForValue(@_);
1711 sub IncludeContentForValue{
1713 $self->_URLTemplate('IncludeContentForValue', @_);
1718 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1720 Gets or sets the C<LinkValueTo> for this custom field. RT
1721 uses this field to make custom field values into hyperlinks in the user's
1722 browser as they display records with custom fields in RT.
1727 sub SetLinkValueTo {
1728 shift->LinkValueTo(@_);
1733 $self->_URLTemplate('LinkValueTo', @_);
1738 =head2 _URLTemplate NAME [VALUE]
1740 With one argument, returns the _URLTemplate named C<NAME>, but only if
1741 the current user has the right to see this custom field.
1743 With two arguments, attemptes to set the relevant template value.
1749 my $template_name = shift;
1753 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1754 return ( 0, $self->loc('Permission Denied') );
1756 $self->SetAttribute( Name => $template_name, Content => $value );
1757 return ( 1, $self->loc('Updated') );
1759 unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1763 my @attr = $self->Attributes->Named($template_name);
1764 my $attr = shift @attr;
1766 if ($attr) { return $attr->Content }
1775 return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1776 unless defined $value and length $value;
1778 my $cf = RT::CustomField->new( $self->CurrentUser );
1779 $cf->SetContextObject( $self->ContextObject );
1780 $cf->Load( ref $value ? $value->id : $value );
1782 return (0, "Permission denied")
1783 unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
1785 # XXX: Remove this restriction once we support lists and cascaded selects
1786 if ( $self->RenderType =~ /List/ ) {
1787 return (0, $self->loc("We can't currently render as a List when basing categories on another custom field. Please use another render type."));
1790 return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1796 my $obj = RT::CustomField->new( $self->CurrentUser );
1797 $obj->SetContextObject( $self->ContextObject );
1798 if ( $self->BasedOn ) {
1799 $obj->Load( $self->BasedOn );
1806 my $tag = $self->FirstAttribute( 'UILocation' );
1807 return $tag ? $tag->Content : '';
1814 return $self->SetAttribute( Name => 'UILocation', Content => $tag );
1817 return $self->DeleteAttribute('UILocation');
1828 Returns the current value of id.
1829 (In the database, id is stored as int(11).)
1837 Returns the current value of Name.
1838 (In the database, Name is stored as varchar(200).)
1842 =head2 SetName VALUE
1846 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1847 (In the database, Name will be stored as a varchar(200).)
1855 Returns the current value of Type.
1856 (In the database, Type is stored as varchar(200).)
1860 =head2 SetType VALUE
1864 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1865 (In the database, Type will be stored as a varchar(200).)
1873 Returns the current value of RenderType.
1874 (In the database, RenderType is stored as varchar(64).)
1878 =head2 SetRenderType VALUE
1881 Set RenderType to VALUE.
1882 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1883 (In the database, RenderType will be stored as a varchar(64).)
1891 Returns the current value of MaxValues.
1892 (In the database, MaxValues is stored as int(11).)
1896 =head2 SetMaxValues VALUE
1899 Set MaxValues to VALUE.
1900 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1901 (In the database, MaxValues will be stored as a int(11).)
1909 Returns the current value of Pattern.
1910 (In the database, Pattern is stored as text.)
1914 =head2 SetPattern VALUE
1917 Set Pattern to VALUE.
1918 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1919 (In the database, Pattern will be stored as a text.)
1927 Returns the current value of Repeated.
1928 (In the database, Repeated is stored as smallint(6).)
1932 =head2 SetRepeated VALUE
1935 Set Repeated to VALUE.
1936 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1937 (In the database, Repeated will be stored as a smallint(6).)
1945 Returns the current value of BasedOn.
1946 (In the database, BasedOn is stored as int(11).)
1950 =head2 SetBasedOn VALUE
1953 Set BasedOn to VALUE.
1954 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1955 (In the database, BasedOn will be stored as a int(11).)
1963 Returns the current value of Description.
1964 (In the database, Description is stored as varchar(255).)
1968 =head2 SetDescription VALUE
1971 Set Description to VALUE.
1972 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1973 (In the database, Description will be stored as a varchar(255).)
1981 Returns the current value of SortOrder.
1982 (In the database, SortOrder is stored as int(11).)
1986 =head2 SetSortOrder VALUE
1989 Set SortOrder to VALUE.
1990 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1991 (In the database, SortOrder will be stored as a int(11).)
1999 Returns the current value of LookupType.
2000 (In the database, LookupType is stored as varchar(255).)
2004 =head2 SetLookupType VALUE
2007 Set LookupType to VALUE.
2008 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2009 (In the database, LookupType will be stored as a varchar(255).)
2017 Returns the current value of Creator.
2018 (In the database, Creator is stored as int(11).)
2026 Returns the current value of Created.
2027 (In the database, Created is stored as datetime.)
2033 =head2 LastUpdatedBy
2035 Returns the current value of LastUpdatedBy.
2036 (In the database, LastUpdatedBy is stored as int(11).)
2044 Returns the current value of LastUpdated.
2045 (In the database, LastUpdated is stored as datetime.)
2053 Returns the current value of Disabled.
2054 (In the database, Disabled is stored as smallint(6).)
2058 =head2 SetDisabled VALUE
2061 Set Disabled to VALUE.
2062 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2063 (In the database, Disabled will be stored as a smallint(6).)
2070 sub _CoreAccessible {
2074 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2076 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
2078 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
2080 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
2082 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2084 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''},
2086 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
2088 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2090 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
2092 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2094 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
2096 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2098 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
2100 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2102 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
2104 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
2110 RT::Base->_ImportOverlays();