1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2013 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;
54 use Scalar::Util 'blessed';
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
178 labels => [ 'Enter multiple IP addresses', # loc
179 'Enter one IP address', # loc
180 'Enter up to [_1] IP addresses', # loc
187 labels => [ 'Enter multiple IP address ranges', # loc
188 'Enter one IP address range', # loc
189 'Enter up to [_1] IP address ranges', # loc
195 our %FRIENDLY_OBJECT_TYPES = ();
197 RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket' => "Tickets", ); #loc
198 RT::CustomField->_ForObjectType(
199 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", ); #loc
200 RT::CustomField->_ForObjectType( 'RT::User' => "Users", ); #loc
201 RT::CustomField->_ForObjectType( 'RT::Queue' => "Queues", ); #loc
202 RT::CustomField->_ForObjectType( 'RT::Group' => "Groups", ); #loc
205 SeeCustomField => 'View custom fields', # loc_pair
206 AdminCustomField => 'Create, modify and delete custom fields', # loc_pair
207 AdminCustomFieldValues => 'Create, modify and delete custom fields values', # loc_pair
208 ModifyCustomField => 'Add, modify and delete custom field values for objects' # loc_pair
211 our $RIGHT_CATEGORIES = {
212 SeeCustomField => 'General',
213 AdminCustomField => 'Admin',
214 AdminCustomFieldValues => 'Admin',
215 ModifyCustomField => 'Staff',
218 # Tell RT::ACE that this sort of object can get acls granted
219 $RT::ACE::OBJECT_TYPES{'RT::CustomField'} = 1;
221 __PACKAGE__->AddRights(%$RIGHTS);
222 __PACKAGE__->AddRightCategories(%$RIGHT_CATEGORIES);
224 =head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...]
226 Adds the given rights to the list of possible rights. This method
227 should be called during server startup, not at runtime.
234 $RIGHTS = { %$RIGHTS, %new };
235 %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES,
236 map { lc($_) => $_ } keys %new);
239 sub AvailableRights {
244 =head2 RightCategories
246 Returns a hashref where the keys are rights for this type of object and the
247 values are the category (General, Staff, Admin) the right falls into.
251 sub RightCategories {
252 return $RIGHT_CATEGORIES;
255 =head2 AddRightCategories C<RIGHT>, C<CATEGORY> [, ...]
257 Adds the given right and category pairs to the list of right categories. This
258 method should be called during server startup, not at runtime.
262 sub AddRightCategories {
263 my $self = shift if ref $_[0] or $_[0] eq __PACKAGE__;
265 $RIGHT_CATEGORIES = { %$RIGHT_CATEGORIES, %new };
270 RT::CustomField_Overlay - overlay for RT::CustomField
274 =head1 'CORE' METHODS
276 =head2 Create PARAMHASH
278 Create takes a hash of values and creates a row in the database:
283 varchar(255) 'Pattern'.
284 smallint(6) 'Repeated'.
285 varchar(255) 'Description'.
287 varchar(255) 'LookupType'.
288 smallint(6) 'Disabled'.
290 C<LookupType> is generally the result of either
291 C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType>.
307 IncludeContentForValue => '',
311 unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) {
312 return (0, $self->loc('Permission Denied'));
315 if ( $args{TypeComposite} ) {
316 @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
318 elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) {
319 # old style Type string
320 $args{'MaxValues'} = $1 ? 1 : 0;
322 $args{'MaxValues'} = int $args{'MaxValues'};
324 if ( !exists $args{'Queue'}) {
325 # do nothing -- things below are strictly backward compat
327 elsif ( ! $args{'Queue'} ) {
328 unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
329 return ( 0, $self->loc('Permission Denied') );
331 $args{'LookupType'} = 'RT::Queue-RT::Ticket';
334 my $queue = RT::Queue->new($self->CurrentUser);
335 $queue->Load($args{'Queue'});
336 unless ($queue->Id) {
337 return (0, $self->loc("Queue not found"));
339 unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
340 return ( 0, $self->loc('Permission Denied') );
342 $args{'LookupType'} = 'RT::Queue-RT::Ticket';
343 $args{'Queue'} = $queue->Id;
346 my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} );
347 return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok;
349 if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) {
350 $RT::Logger->debug("Support for 'multiple' Texts or Comboboxes is not implemented");
351 $args{'MaxValues'} = 1;
354 if ( $args{'RenderType'} ||= undef ) {
355 my $composite = join '-', @args{'Type', 'MaxValues'};
356 return (0, $self->loc("This custom field has no Render Types"))
357 unless $self->HasRenderTypes( $composite );
359 if ( $args{'RenderType'} eq $self->DefaultRenderType( $composite ) ) {
360 $args{'RenderType'} = undef;
362 return (0, $self->loc("Invalid Render Type") )
363 unless grep $_ eq $args{'RenderType'}, $self->RenderTypes( $composite );
367 $args{'ValuesClass'} = undef if ($args{'ValuesClass'} || '') eq 'RT::CustomFieldValues';
368 if ( $args{'ValuesClass'} ||= undef ) {
369 return (0, $self->loc("This Custom Field can not have list of values"))
370 unless $self->IsSelectionType( $args{'Type'} );
372 unless ( $self->ValidateValuesClass( $args{'ValuesClass'} ) ) {
373 return (0, $self->loc("Invalid Custom Field values source"));
377 (my $rv, $msg) = $self->SUPER::Create(
378 Name => $args{'Name'},
379 Type => $args{'Type'},
380 RenderType => $args{'RenderType'},
381 MaxValues => $args{'MaxValues'},
382 Pattern => $args{'Pattern'},
383 BasedOn => $args{'BasedOn'},
384 ValuesClass => $args{'ValuesClass'},
385 Description => $args{'Description'},
386 Disabled => $args{'Disabled'},
387 LookupType => $args{'LookupType'},
388 Repeated => $args{'Repeated'},
392 if ( exists $args{'LinkValueTo'}) {
393 $self->SetLinkValueTo($args{'LinkValueTo'});
396 if ( exists $args{'IncludeContentForValue'}) {
397 $self->SetIncludeContentForValue($args{'IncludeContentForValue'});
400 return ($rv, $msg) unless exists $args{'Queue'};
402 # Compat code -- create a new ObjectCustomField mapping
403 my $OCF = RT::ObjectCustomField->new( $self->CurrentUser );
405 CustomField => $self->Id,
406 ObjectId => $args{'Queue'},
415 Load a custom field. If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
421 my $id = shift || '';
423 if ( $id =~ /^\d+$/ ) {
424 return $self->SUPER::Load( $id );
426 return $self->LoadByName( Name => $id );
432 =head2 LoadByName (Queue => QUEUEID, Name => NAME)
434 Loads the Custom field named NAME.
436 Will load a Disabled Custom Field even if there is a non-disabled Custom Field
439 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
441 If the Queue parameter is '0', look for global ticket custom fields.
443 If no queue parameter is specified, look for any and all custom fields with this name.
445 BUG/TODO, this won't let you specify that you only want user or group CFs.
449 # Compatibility for API change after 3.0 beta 1
450 *LoadNameAndQueue = \&LoadByName;
451 # Change after 3.4 beta.
452 *LoadByNameAndQueue = \&LoadByName;
462 unless ( defined $args{'Name'} && length $args{'Name'} ) {
463 $RT::Logger->error("Couldn't load Custom Field without Name");
464 return wantarray ? (0, $self->loc("No name provided")) : 0;
467 # if we're looking for a queue by name, make it a number
468 if ( defined $args{'Queue'} && ($args{'Queue'} =~ /\D/ || !$self->ContextObject) ) {
469 my $QueueObj = RT::Queue->new( $self->CurrentUser );
470 $QueueObj->Load( $args{'Queue'} );
471 $args{'Queue'} = $QueueObj->Id;
472 $self->SetContextObject( $QueueObj )
473 unless $self->ContextObject;
476 # XXX - really naive implementation. Slow. - not really. still just one query
478 my $CFs = RT::CustomFields->new( $self->CurrentUser );
479 $CFs->SetContextObject( $self->ContextObject );
480 my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
481 $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
482 # Don't limit to queue if queue is 0. Trying to do so breaks
483 # RT::Group type CFs.
484 if ( defined $args{'Queue'} ) {
485 $CFs->LimitToQueue( $args{'Queue'} );
488 # When loading by name, we _can_ load disabled fields, but prefer
489 # non-disabled fields.
492 { FIELD => "Disabled", ORDER => 'ASC' },
495 # We only want one entry.
496 $CFs->RowsPerPage(1);
498 # version before 3.8 just returns 0, so we need to test if wantarray to be
499 # backward compatible.
500 return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
502 return $self->LoadById( $first->id );
508 =head2 Custom field values
512 Return a object (collection) of all acceptable values for this Custom Field.
513 Class of the object can vary and depends on the return value
514 of the C<ValuesClass> method.
518 *ValuesObj = \&Values;
523 my $class = $self->ValuesClass;
524 if ( $class ne 'RT::CustomFieldValues') {
525 eval "require $class" or die "$@";
527 my $cf_values = $class->new( $self->CurrentUser );
528 # if the user has no rights, return an empty object
529 if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
530 $cf_values->LimitToCustomField( $self->Id );
532 $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
540 Create a new value for this CustomField. Takes a paramhash containing the elements Name, Description and SortOrder
548 unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) {
549 return (0, $self->loc('Permission Denied'));
553 if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
554 return (0, $self->loc("Can't add a custom field value without a name"));
557 my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
558 return $newval->Create( %args, CustomField => $self->Id );
564 =head3 DeleteValue ID
566 Deletes a value from this custom field by id.
568 Does not remove this value for any article which has had it selected
575 unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) {
576 return (0, $self->loc('Permission Denied'));
579 my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
580 $val_to_del->Load( $id );
581 unless ( $val_to_del->Id ) {
582 return (0, $self->loc("Couldn't find that value"));
584 unless ( $val_to_del->CustomField == $self->Id ) {
585 return (0, $self->loc("That is not a value for this custom field"));
588 my $retval = $val_to_del->Delete;
590 return (0, $self->loc("Custom field value could not be deleted"));
592 return ($retval, $self->loc("Custom field value deleted"));
596 =head2 ValidateQueue Queue
598 Make sure that the name specified is valid
606 return 0 unless length $value;
608 return $self->SUPER::ValidateName($value);
611 =head2 ValidateQueue Queue
613 Make sure that the queue specified is a valid queue name
621 return undef unless defined $id;
622 # 0 means "Global" null would _not_ be ok.
623 return 1 if $id eq '0';
625 my $q = RT::Queue->new( RT->SystemUser );
627 return undef unless $q->id;
635 Retuns an array of the types of CustomField that are supported
640 return (sort {(($FieldTypes{$a}{sort_order}||999) <=> ($FieldTypes{$b}{sort_order}||999)) or ($a cmp $b)} keys %FieldTypes);
644 =head2 IsSelectionType
646 Retuns a boolean value indicating whether the C<Values> method makes sense
647 to this Custom Field.
651 sub IsSelectionType {
653 my $type = @_? shift : $self->Type;
654 return undef unless $type;
655 return $FieldTypes{$type}->{selection_type};
660 =head2 IsExternalValues
664 sub IsExternalValues {
666 return 0 unless $self->IsSelectionType( @_ );
667 return $self->ValuesClass eq 'RT::CustomFieldValues'? 0 : 1;
672 return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues';
677 my $class = shift || 'RT::CustomFieldValues';
679 if ( $class eq 'RT::CustomFieldValues' ) {
680 return $self->_Set( Field => 'ValuesClass', Value => undef, @_ );
683 return (0, $self->loc("This Custom Field can not have list of values"))
684 unless $self->IsSelectionType;
686 unless ( $self->ValidateValuesClass( $class ) ) {
687 return (0, $self->loc("Invalid Custom Field values source"));
689 return $self->_Set( Field => 'ValuesClass', Value => $class, @_ );
692 sub ValidateValuesClass {
696 return 1 if !$class || $class eq 'RT::CustomFieldValues';
697 return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesSources');
702 =head2 FriendlyType [TYPE, MAX_VALUES]
704 Returns a localized human-readable version of the custom field type.
705 If a custom field type is specified as the parameter, the friendly type for that type will be returned
712 my $type = @_ ? shift : $self->Type;
713 my $max = @_ ? shift : $self->MaxValues;
714 $max = 0 unless $max;
716 if (my $friendly_type = $FieldTypes{$type}->{labels}->[$max>2 ? 2 : $max]) {
717 return ( $self->loc( $friendly_type, $max ) );
720 return ( $self->loc( $type ) );
724 sub FriendlyTypeComposite {
726 my $composite = shift || $self->TypeComposite;
727 return $self->FriendlyType(split(/-/, $composite, 2));
731 =head2 ValidateType TYPE
733 Takes a single string. returns true if that string is a value
743 if ( $type =~ s/(?:Single|Multiple)$// ) {
744 $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
747 if ( $FieldTypes{$type} ) {
759 if ($type =~ s/(?:(Single)|Multiple)$//) {
760 $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
761 $self->SetMaxValues($1 ? 1 : 0);
763 $self->_Set(Field => 'Type', Value =>$type);
766 =head2 SetPattern STRING
768 Takes a single string representing a regular expression. Performs basic
769 validation on that regex, and sets the C<Pattern> field for the CF if it
778 my ($ok, $msg) = $self->_IsValidRegex($regex);
780 return $self->_Set(Field => 'Pattern', Value => $regex);
783 return (0, $self->loc("Invalid pattern: [_1]", $msg));
787 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
789 Tests if the string contains an invalid regex.
795 my $regex = shift or return (1, 'valid');
798 local $SIG{__DIE__} = sub { 1 };
799 local $SIG{__WARN__} = sub { 1 };
801 if (eval { qr/$regex/; 1 }) {
806 $err =~ s{[,;].*}{}; # strip debug info from error
814 Returns true if this CustomField only accepts a single value.
815 Returns false if it accepts multiple values
821 if (($self->MaxValues||0) == 1) {
829 sub UnlimitedValues {
831 if (($self->MaxValues||0) == 0) {
840 =head2 CurrentUserHasRight RIGHT
842 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
846 sub CurrentUserHasRight {
850 return $self->CurrentUser->HasRight(
856 =head2 ACLEquivalenceObjects
858 Returns list of objects via which users can get rights on this custom field. For custom fields
859 these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
863 sub ACLEquivalenceObjects {
866 my $ctx = $self->ContextObject
868 return ($ctx, $ctx->ACLEquivalenceObjects);
871 =head2 ContextObject and SetContextObject
873 Set or get a context for this object. It can be ticket, queue or another object
874 this CF applies to. Used for ACL control, for example SeeCustomField can be granted on
875 queue level to allow people to see all fields applied to the queue.
879 sub SetContextObject {
881 return $self->{'context_object'} = shift;
886 return $self->{'context_object'};
889 sub ValidContextType {
894 $valid{$_}++ for split '-', $self->LookupType;
895 delete $valid{'RT::Transaction'};
897 return $valid{$class};
900 =head2 LoadContextObject
902 Takes an Id for a Context Object and loads the right kind of RT::Object
903 for this particular Custom Field (based on the LookupType) and returns it.
904 This is a good way to ensure you don't try to use a Queue as a Context
905 Object on a User Custom Field.
909 sub LoadContextObject {
912 my $contextid = shift;
914 unless ( $self->ValidContextType($type) ) {
915 RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id);
919 my $context_object = $type->new( $self->CurrentUser );
920 my ($id, $msg) = $context_object->LoadById( $contextid );
922 RT->Logger->debug("Invalid ContextObject id: $msg");
925 return $context_object;
928 =head2 ValidateContextObject
930 Ensure that a given ContextObject applies to this Custom Field.
931 For custom fields that are assigned to Queues or to Classes, this checks that the Custom
932 Field is actually applied to that objects. For Global Custom Fields, it returns true
933 as long as the Object is of the right type, because you may be using
934 your permissions on a given Queue of Class to see a Global CF.
935 For CFs that are only applied Globally, you don't need a ContextObject.
939 sub ValidateContextObject {
943 return 1 if $self->IsApplied(0);
945 # global only custom fields don't have objects
946 # that should be used as context objects.
947 return if $self->ApplyGlobally;
949 # Otherwise, make sure we weren't passed a user object that we're
950 # supposed to treat as a queue.
951 return unless $self->ValidContextType(ref $object);
953 # Check that it is applied correctly
954 my ($applied_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects);
955 return unless $applied_to;
956 return $self->IsApplied($applied_to->id);
963 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
964 return ( 0, $self->loc('Permission Denied') );
966 return $self->SUPER::_Set( @_ );
974 Takes the name of a table column.
975 Returns its value as a string, if the user passes an ACL check
981 return undef unless $self->id;
983 # we need to do the rights check
984 unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
986 "Permission denied. User #". $self->CurrentUser->id
987 ." has no SeeCustomField right on CF #". $self->id
991 return $self->__Value( @_ );
998 1 will cause this custom field to no longer be avaialble for objects.
999 0 will re-enable this field.
1004 =head2 SetTypeComposite
1006 Set this custom field's type and maximum values as a composite value
1010 sub SetTypeComposite {
1012 my $composite = shift;
1014 my $old = $self->TypeComposite;
1016 my ($type, $max_values) = split(/-/, $composite, 2);
1017 if ( $type ne $self->Type ) {
1018 my ($status, $msg) = $self->SetType( $type );
1019 return ($status, $msg) unless $status;
1021 if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
1022 my ($status, $msg) = $self->SetMaxValues( $max_values );
1023 return ($status, $msg) unless $status;
1025 my $render = $self->RenderType;
1026 if ( $render and not grep { $_ eq $render } $self->RenderTypes ) {
1027 # We switched types and our render type is no longer valid, so unset it
1028 # and use the default
1029 $self->SetRenderType( undef );
1031 return 1, $self->loc(
1032 "Type changed from '[_1]' to '[_2]'",
1033 $self->FriendlyTypeComposite( $old ),
1034 $self->FriendlyTypeComposite( $composite ),
1038 =head2 TypeComposite
1040 Returns a composite value composed of this object's type and maximum values
1047 return join '-', ($self->Type || ''), ($self->MaxValues || 0);
1050 =head2 TypeComposites
1052 Returns an array of all possible composite values for custom fields.
1056 sub TypeComposites {
1058 return grep !/(?:[Tt]ext|Combobox|Date|DateTime)-0/, map { ("$_-1", "$_-0") } $self->Types;
1063 Returns the type of form widget to render for this custom field. Currently
1064 this only affects fields which return true for L</HasRenderTypes>.
1070 return '' unless $self->HasRenderTypes;
1072 return $self->_Value( 'RenderType', @_ )
1073 || $self->DefaultRenderType;
1076 =head2 SetRenderType TYPE
1078 Sets this custom field's render type.
1085 return (0, $self->loc("This custom field has no Render Types"))
1086 unless $self->HasRenderTypes;
1088 if ( !$type || $type eq $self->DefaultRenderType ) {
1089 return $self->_Set( Field => 'RenderType', Value => undef, @_ );
1092 if ( not grep { $_ eq $type } $self->RenderTypes ) {
1093 return (0, $self->loc("Invalid Render Type for custom field of type [_1]",
1094 $self->FriendlyType));
1097 # XXX: Remove this restriction once we support lists and cascaded selects
1098 if ( $self->BasedOnObj->id and $type =~ /List/ ) {
1099 return (0, $self->loc("We can't currently render as a List when basing categories on another custom field. Please use another render type."));
1102 return $self->_Set( Field => 'RenderType', Value => $type, @_ );
1105 =head2 DefaultRenderType [TYPE COMPOSITE]
1107 Returns the default render type for this custom field's type or the TYPE
1108 COMPOSITE specified as an argument.
1112 sub DefaultRenderType {
1114 my $composite = @_ ? shift : $self->TypeComposite;
1115 my ($type, $max) = split /-/, $composite, 2;
1116 return unless $type and $self->HasRenderTypes($composite);
1117 return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0];
1120 =head2 HasRenderTypes [TYPE_COMPOSITE]
1122 Returns a boolean value indicating whether the L</RenderTypes> and
1123 L</RenderType> methods make sense for this custom field.
1125 Currently true only for type C<Select>.
1129 sub HasRenderTypes {
1131 my ($type, $max) = split /-/, (@_ ? shift : $self->TypeComposite), 2;
1132 return undef unless $type;
1133 return defined $FieldTypes{$type}->{render_types}
1134 ->{ $max == 1 ? 'single' : 'multiple' };
1137 =head2 RenderTypes [TYPE COMPOSITE]
1139 Returns the valid render types for this custom field's type or the TYPE
1140 COMPOSITE specified as an argument.
1146 my $composite = @_ ? shift : $self->TypeComposite;
1147 my ($type, $max) = split /-/, $composite, 2;
1148 return unless $type and $self->HasRenderTypes($composite);
1149 return @{$FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }};
1152 =head2 SetLookupType
1154 Autrijus: care to doc how LookupTypes work?
1161 if ( $lookup ne $self->LookupType ) {
1162 # Okay... We need to invalidate our existing relationships
1163 my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
1164 $ObjectCustomFields->LimitToCustomField($self->Id);
1165 $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
1167 return $self->_Set(Field => 'LookupType', Value =>$lookup);
1172 Returns an array of LookupTypes available
1179 return keys %FRIENDLY_OBJECT_TYPES;
1182 my @FriendlyObjectTypes = (
1183 "[_1] objects", # loc
1184 "[_1]'s [_2] objects", # loc
1185 "[_1]'s [_2]'s [_3] objects", # loc
1188 =head2 FriendlyLookupType
1190 Returns a localized description of the type of this custom field
1194 sub FriendlyLookupType {
1196 my $lookup = shift || $self->LookupType;
1198 return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
1199 if (defined $FRIENDLY_OBJECT_TYPES{$lookup} );
1201 my @types = map { s/^RT::// ? $self->loc($_) : $_ }
1202 grep { defined and length }
1203 split( /-/, $lookup )
1205 return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
1208 =head1 RecordClassFromLookupType
1210 Returns the type of Object referred to by ObjectCustomFields' ObjectId column
1212 Optionally takes a LookupType to use instead of using the value on the loaded
1213 record. In this case, the method may be called on the class instead of an
1218 sub RecordClassFromLookupType {
1220 my $type = shift || $self->LookupType;
1221 my ($class) = ($type =~ /^([^-]+)/);
1223 if (blessed($self) and $self->LookupType eq $type) {
1225 "Custom Field #". $self->id
1226 ." has incorrect LookupType '$type'"
1229 RT->Logger->error("Invalid LookupType passed as argument: $type");
1236 =head1 ObjectTypeFromLookupType
1238 Returns the ObjectType used in ObjectCustomFieldValues rows for this CF
1240 Optionally takes a LookupType to use instead of using the value on the loaded
1241 record. In this case, the method may be called on the class instead of an
1246 sub ObjectTypeFromLookupType {
1248 my $type = shift || $self->LookupType;
1249 my ($class) = ($type =~ /([^-]+)$/);
1251 if (blessed($self) and $self->LookupType eq $type) {
1253 "Custom Field #". $self->id
1254 ." has incorrect LookupType '$type'"
1257 RT->Logger->error("Invalid LookupType passed as argument: $type");
1264 sub CollectionClassFromLookupType {
1267 my $record_class = $self->RecordClassFromLookupType;
1268 return undef unless $record_class;
1270 my $collection_class;
1271 if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
1272 $collection_class = $record_class.'Collection';
1273 } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
1274 $collection_class = $record_class.'es';
1275 } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
1276 $collection_class = $record_class.'s';
1278 $RT::Logger->error("Can not find a collection class for record class '$record_class'");
1281 return $collection_class;
1284 =head1 ApplyGlobally
1286 Certain custom fields (users, groups) should only be applied globally
1287 but rather than regexing in code for LookupType =~ RT::Queue, we'll codify
1295 return ($self->LookupType =~ /^RT::(?:Group|User)/io);
1301 Returns collection with objects this custom field is applied to.
1302 Class of the collection depends on L</LookupType>.
1303 See all L</NotAppliedTo> .
1305 Doesn't takes into account if object is applied globally.
1312 my ($res, $ocfs_alias) = $self->_AppliedTo;
1313 return $res unless $res;
1316 ALIAS => $ocfs_alias,
1318 OPERATOR => 'IS NOT',
1327 Returns collection with objects this custom field is not applied to.
1328 Class of the collection depends on L</LookupType>.
1329 See all L</AppliedTo> .
1331 Doesn't takes into account if object is applied globally.
1338 my ($res, $ocfs_alias) = $self->_AppliedTo;
1339 return $res unless $res;
1342 ALIAS => $ocfs_alias,
1354 my ($class) = $self->CollectionClassFromLookupType;
1355 return undef unless $class;
1357 my $res = $class->new( $self->CurrentUser );
1359 # If CF is a Group CF, only display user-defined groups
1360 if ( $class eq 'RT::Groups' ) {
1361 $res->LimitToUserDefinedGroups;
1364 $res->OrderBy( FIELD => 'Name' );
1365 my $ocfs_alias = $res->Join(
1369 TABLE2 => 'ObjectCustomFields',
1370 FIELD2 => 'ObjectId',
1373 LEFTJOIN => $ocfs_alias,
1374 ALIAS => $ocfs_alias,
1375 FIELD => 'CustomField',
1378 return ($res, $ocfs_alias);
1383 Takes object id and returns corresponding L<RT::ObjectCustomField>
1384 record if this custom field is applied to the object. Use 0 to check
1385 if custom field is applied globally.
1392 my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1393 $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1394 return undef unless $ocf->id;
1398 =head2 AddToObject OBJECT
1400 Add this custom field as a custom field for a single object, such as a queue or group.
1410 my $id = $object->Id || 0;
1412 unless (index($self->LookupType, ref($object)) == 0) {
1413 return ( 0, $self->loc('Lookup type mismatch') );
1416 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1417 return ( 0, $self->loc('Permission Denied') );
1420 if ( $self->IsApplied( $id ) ) {
1421 return ( 0, $self->loc("Custom field is already applied to the object") );
1426 return (0, $self->loc("Couldn't apply custom field to an object as it's global already") )
1427 if $self->IsApplied( 0 );
1430 my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
1431 $applied->LimitToCustomField( $self->id );
1432 while ( my $record = $applied->Next ) {
1437 my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1438 my ( $oid, $msg ) = $ocf->Create(
1439 ObjectId => $id, CustomField => $self->id,
1441 return ( $oid, $msg );
1445 =head2 RemoveFromObject OBJECT
1447 Remove this custom field for a single object, such as a queue or group.
1453 sub RemoveFromObject {
1456 my $id = $object->Id || 0;
1458 unless (index($self->LookupType, ref($object)) == 0) {
1459 return ( 0, $self->loc('Object type mismatch') );
1462 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1463 return ( 0, $self->loc('Permission Denied') );
1466 my $ocf = $self->IsApplied( $id );
1468 return ( 0, $self->loc("This custom field does not apply to that object") );
1471 # XXX: Delete doesn't return anything
1472 my ( $oid, $msg ) = $ocf->Delete;
1473 return ( $oid, $msg );
1477 =head2 AddValueForObject HASH
1479 Adds a custom field value for a record object of some kind.
1480 Takes a param hash of
1494 sub AddValueForObject {
1499 LargeContent => undef,
1500 ContentType => undef,
1503 my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1505 unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1506 return ( 0, $self->loc('Permission Denied') );
1509 unless ( $self->MatchPattern($args{'Content'}) ) {
1510 return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1513 $RT::Handle->BeginTransaction;
1515 if ( $self->MaxValues ) {
1516 my $current_values = $self->ValuesForObject($obj);
1517 my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1519 # (The +1 is for the new value we're adding)
1521 # If we have a set of current values and we've gone over the maximum
1522 # allowed number of values, we'll need to delete some to make room.
1523 # which former values are blown away is not guaranteed
1525 while ($extra_values) {
1526 my $extra_item = $current_values->Next;
1527 unless ( $extra_item->id ) {
1528 $RT::Logger->crit( "We were just asked to delete "
1529 ."a custom field value that doesn't exist!" );
1530 $RT::Handle->Rollback();
1533 $extra_item->Delete;
1538 if (my $canonicalizer = $self->can('_CanonicalizeValue'.$self->Type)) {
1539 $canonicalizer->($self, \%args);
1544 my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1545 my ($val, $msg) = $newval->Create(
1546 ObjectType => ref($obj),
1547 ObjectId => $obj->Id,
1548 Content => $args{'Content'},
1549 LargeContent => $args{'LargeContent'},
1550 ContentType => $args{'ContentType'},
1551 CustomField => $self->Id
1555 $RT::Handle->Rollback();
1556 return ($val, $self->loc("Couldn't create record: [_1]", $msg));
1559 $RT::Handle->Commit();
1566 sub _CanonicalizeValueDateTime {
1569 my $DateObj = RT::Date->new( $self->CurrentUser );
1570 $DateObj->Set( Format => 'unknown',
1571 Value => $args->{'Content'} );
1572 $args->{'Content'} = $DateObj->ISO;
1575 # For date, we need to store Content as ISO date
1576 sub _CanonicalizeValueDate {
1580 # in case user input date with time, let's omit it by setting timezone
1581 # to utc so "hour" won't affect "day"
1582 my $DateObj = RT::Date->new( $self->CurrentUser );
1583 $DateObj->Set( Format => 'unknown',
1584 Value => $args->{'Content'},
1586 $args->{'Content'} = $DateObj->Date( Timezone => 'user' );
1589 =head2 MatchPattern STRING
1591 Tests the incoming string against the Pattern of this custom field object
1592 and returns a boolean; returns true if the Pattern is empty.
1598 my $regex = $self->Pattern or return 1;
1600 return (( defined $_[0] ? $_[0] : '') =~ $regex);
1606 =head2 FriendlyPattern
1608 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1613 sub FriendlyPattern {
1615 my $regex = $self->Pattern;
1617 return '' unless length $regex;
1618 if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1619 return '[' . $self->loc($1) . ']';
1629 =head2 DeleteValueForObject HASH
1631 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1633 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1637 sub DeleteValueForObject {
1639 my %args = ( Object => undef,
1645 unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1646 return (0, $self->loc('Permission Denied'));
1649 my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1651 if (my $id = $args{'Id'}) {
1654 unless ($oldval->id) {
1655 $oldval->LoadByObjectContentAndCustomField(
1656 Object => $args{'Object'},
1657 Content => $args{'Content'},
1658 CustomField => $self->Id,
1663 # check to make sure we found it
1664 unless ($oldval->Id) {
1665 return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1668 # for single-value fields, we need to validate that empty string is a valid value for it
1669 if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1670 return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1675 my $ret = $oldval->Delete();
1677 return(0, $self->loc("Custom field value could not be found"));
1679 return($oldval->Id, $self->loc("Custom field value deleted"));
1683 =head2 ValuesForObject OBJECT
1685 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT
1689 sub ValuesForObject {
1693 my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
1694 unless ($self->id and $self->CurrentUserHasRight('SeeCustomField')) {
1695 # Return an empty object if they have no rights to see
1696 $values->Limit( FIELD => "id", VALUE => 0, SUBCLAUSE => "ACL" );
1700 $values->LimitToCustomField($self->Id);
1701 $values->LimitToObject($object);
1707 =head2 _ForObjectType PATH FRIENDLYNAME
1709 Tell RT that a certain object accepts custom fields
1713 'RT::Queue-RT::Ticket' => "Tickets", # loc
1714 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc
1715 'RT::User' => "Users", # loc
1716 'RT::Group' => "Groups", # loc
1717 'RT::Queue' => "Queues", # loc
1719 This is a class method.
1723 sub _ForObjectType {
1726 my $friendly_name = shift;
1728 $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1733 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1735 Gets or sets the C<IncludeContentForValue> for this custom field. RT
1736 uses this field to automatically include content into the user's browser
1737 as they display records with custom fields in RT.
1741 sub SetIncludeContentForValue {
1742 shift->IncludeContentForValue(@_);
1744 sub IncludeContentForValue{
1746 $self->_URLTemplate('IncludeContentForValue', @_);
1751 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1753 Gets or sets the C<LinkValueTo> for this custom field. RT
1754 uses this field to make custom field values into hyperlinks in the user's
1755 browser as they display records with custom fields in RT.
1760 sub SetLinkValueTo {
1761 shift->LinkValueTo(@_);
1766 $self->_URLTemplate('LinkValueTo', @_);
1771 =head2 _URLTemplate NAME [VALUE]
1773 With one argument, returns the _URLTemplate named C<NAME>, but only if
1774 the current user has the right to see this custom field.
1776 With two arguments, attemptes to set the relevant template value.
1782 my $template_name = shift;
1786 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1787 return ( 0, $self->loc('Permission Denied') );
1789 $self->SetAttribute( Name => $template_name, Content => $value );
1790 return ( 1, $self->loc('Updated') );
1792 unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1796 my @attr = $self->Attributes->Named($template_name);
1797 my $attr = shift @attr;
1799 if ($attr) { return $attr->Content }
1808 return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1809 unless defined $value and length $value;
1811 my $cf = RT::CustomField->new( $self->CurrentUser );
1812 $cf->SetContextObject( $self->ContextObject );
1813 $cf->Load( ref $value ? $value->id : $value );
1815 return (0, "Permission denied")
1816 unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
1818 # XXX: Remove this restriction once we support lists and cascaded selects
1819 if ( $self->RenderType =~ /List/ ) {
1820 return (0, $self->loc("We can't currently render as a List when basing categories on another custom field. Please use another render type."));
1823 return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1829 my $obj = RT::CustomField->new( $self->CurrentUser );
1830 $obj->SetContextObject( $self->ContextObject );
1831 if ( $self->BasedOn ) {
1832 $obj->Load( $self->BasedOn );
1844 Returns the current value of id.
1845 (In the database, id is stored as int(11).)
1853 Returns the current value of Name.
1854 (In the database, Name is stored as varchar(200).)
1858 =head2 SetName VALUE
1862 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1863 (In the database, Name will be stored as a varchar(200).)
1871 Returns the current value of Type.
1872 (In the database, Type is stored as varchar(200).)
1876 =head2 SetType VALUE
1880 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1881 (In the database, Type will be stored as a varchar(200).)
1889 Returns the current value of RenderType.
1890 (In the database, RenderType is stored as varchar(64).)
1894 =head2 SetRenderType VALUE
1897 Set RenderType to VALUE.
1898 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1899 (In the database, RenderType will be stored as a varchar(64).)
1907 Returns the current value of MaxValues.
1908 (In the database, MaxValues is stored as int(11).)
1912 =head2 SetMaxValues VALUE
1915 Set MaxValues to VALUE.
1916 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1917 (In the database, MaxValues will be stored as a int(11).)
1925 Returns the current value of Pattern.
1926 (In the database, Pattern is stored as text.)
1930 =head2 SetPattern VALUE
1933 Set Pattern to VALUE.
1934 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1935 (In the database, Pattern will be stored as a text.)
1943 Returns the current value of Repeated.
1944 (In the database, Repeated is stored as smallint(6).)
1948 =head2 SetRepeated VALUE
1951 Set Repeated to VALUE.
1952 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1953 (In the database, Repeated will be stored as a smallint(6).)
1961 Returns the current value of BasedOn.
1962 (In the database, BasedOn is stored as int(11).)
1966 =head2 SetBasedOn VALUE
1969 Set BasedOn to VALUE.
1970 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1971 (In the database, BasedOn will be stored as a int(11).)
1979 Returns the current value of Description.
1980 (In the database, Description is stored as varchar(255).)
1984 =head2 SetDescription VALUE
1987 Set Description to VALUE.
1988 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1989 (In the database, Description will be stored as a varchar(255).)
1997 Returns the current value of SortOrder.
1998 (In the database, SortOrder is stored as int(11).)
2002 =head2 SetSortOrder VALUE
2005 Set SortOrder to VALUE.
2006 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2007 (In the database, SortOrder will be stored as a int(11).)
2015 Returns the current value of LookupType.
2016 (In the database, LookupType is stored as varchar(255).)
2020 =head2 SetLookupType VALUE
2023 Set LookupType to VALUE.
2024 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2025 (In the database, LookupType will be stored as a varchar(255).)
2033 Returns the current value of Creator.
2034 (In the database, Creator is stored as int(11).)
2042 Returns the current value of Created.
2043 (In the database, Created is stored as datetime.)
2049 =head2 LastUpdatedBy
2051 Returns the current value of LastUpdatedBy.
2052 (In the database, LastUpdatedBy is stored as int(11).)
2060 Returns the current value of LastUpdated.
2061 (In the database, LastUpdated is stored as datetime.)
2069 Returns the current value of Disabled.
2070 (In the database, Disabled is stored as smallint(6).)
2074 =head2 SetDisabled VALUE
2077 Set Disabled to VALUE.
2078 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2079 (In the database, Disabled will be stored as a smallint(6).)
2086 sub _CoreAccessible {
2090 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2092 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
2094 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
2096 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
2098 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2100 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''},
2102 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
2104 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
2106 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2108 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
2110 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2112 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
2114 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2116 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
2118 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2120 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
2122 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
2128 RT::Base->_ImportOverlays();