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/ ) {
478 my $QueueObj = RT::Queue->new( $self->CurrentUser );
479 $QueueObj->Load( $args{'Queue'} );
480 $args{'Queue'} = $QueueObj->Id;
483 # XXX - really naive implementation. Slow. - not really. still just one query
485 my $CFs = RT::CustomFields->new( $self->CurrentUser );
486 $CFs->SetContextObject( $self->ContextObject );
487 my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
488 $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
489 # Don't limit to queue if queue is 0. Trying to do so breaks
490 # RT::Group type CFs.
491 if ( defined $args{'Queue'} ) {
492 $CFs->LimitToQueue( $args{'Queue'} );
495 # When loading by name, we _can_ load disabled fields, but prefer
496 # non-disabled fields.
499 { FIELD => "Disabled", ORDER => 'ASC' },
502 # We only want one entry.
503 $CFs->RowsPerPage(1);
505 # version before 3.8 just returns 0, so we need to test if wantarray to be
506 # backward compatible.
507 return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
509 return $self->LoadById( $first->id );
515 =head2 Custom field values
519 Return a object (collection) of all acceptable values for this Custom Field.
520 Class of the object can vary and depends on the return value
521 of the C<ValuesClass> method.
525 *ValuesObj = \&Values;
530 my $class = $self->ValuesClass;
531 if ( $class ne 'RT::CustomFieldValues') {
532 eval "require $class" or die "$@";
534 my $cf_values = $class->new( $self->CurrentUser );
535 # if the user has no rights, return an empty object
536 if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
537 $cf_values->LimitToCustomField( $self->Id );
545 Create a new value for this CustomField. Takes a paramhash containing the elements Name, Description and SortOrder
553 unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) {
554 return (0, $self->loc('Permission Denied'));
558 if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
559 return (0, $self->loc("Can't add a custom field value without a name"));
562 my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
563 return $newval->Create( %args, CustomField => $self->Id );
569 =head3 DeleteValue ID
571 Deletes a value from this custom field by id.
573 Does not remove this value for any article which has had it selected
580 unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) {
581 return (0, $self->loc('Permission Denied'));
584 my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
585 $val_to_del->Load( $id );
586 unless ( $val_to_del->Id ) {
587 return (0, $self->loc("Couldn't find that value"));
589 unless ( $val_to_del->CustomField == $self->Id ) {
590 return (0, $self->loc("That is not a value for this custom field"));
593 my $retval = $val_to_del->Delete;
595 return (0, $self->loc("Custom field value could not be deleted"));
597 return ($retval, $self->loc("Custom field value deleted"));
601 =head2 ValidateQueue Queue
603 Make sure that the name specified is valid
611 return 0 unless length $value;
613 return $self->SUPER::ValidateName($value);
616 =head2 ValidateQueue Queue
618 Make sure that the queue specified is a valid queue name
626 return undef unless defined $id;
627 # 0 means "Global" null would _not_ be ok.
628 return 1 if $id eq '0';
630 my $q = RT::Queue->new( RT->SystemUser );
632 return undef unless $q->id;
640 Retuns an array of the types of CustomField that are supported
645 return (sort {(($FieldTypes{$a}{sort_order}||999) <=> ($FieldTypes{$b}{sort_order}||999)) or ($a cmp $b)} keys %FieldTypes);
649 =head2 IsSelectionType
651 Retuns a boolean value indicating whether the C<Values> method makes sense
652 to this Custom Field.
656 sub IsSelectionType {
658 my $type = @_? shift : $self->Type;
659 return undef unless $type;
660 return $FieldTypes{$type}->{selection_type};
665 =head2 IsExternalValues
669 sub IsExternalValues {
671 return 0 unless $self->IsSelectionType( @_ );
672 return $self->ValuesClass eq 'RT::CustomFieldValues'? 0 : 1;
677 return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues';
682 my $class = shift || 'RT::CustomFieldValues';
684 if ( $class eq 'RT::CustomFieldValues' ) {
685 return $self->_Set( Field => 'ValuesClass', Value => undef, @_ );
688 return (0, $self->loc("This Custom Field can not have list of values"))
689 unless $self->IsSelectionType;
691 unless ( $self->ValidateValuesClass( $class ) ) {
692 return (0, $self->loc("Invalid Custom Field values source"));
694 return $self->_Set( Field => 'ValuesClass', Value => $class, @_ );
697 sub ValidateValuesClass {
701 return 1 if !defined $class || $class eq 'RT::CustomFieldValues';
702 return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesSources');
707 =head2 FriendlyType [TYPE, MAX_VALUES]
709 Returns a localized human-readable version of the custom field type.
710 If a custom field type is specified as the parameter, the friendly type for that type will be returned
717 my $type = @_ ? shift : $self->Type;
718 my $max = @_ ? shift : $self->MaxValues;
719 $max = 0 unless $max;
721 if (my $friendly_type = $FieldTypes{$type}->{labels}->[$max>2 ? 2 : $max]) {
722 return ( $self->loc( $friendly_type, $max ) );
725 return ( $self->loc( $type ) );
729 sub FriendlyTypeComposite {
731 my $composite = shift || $self->TypeComposite;
732 return $self->FriendlyType(split(/-/, $composite, 2));
736 =head2 ValidateType TYPE
738 Takes a single string. returns true if that string is a value
748 if ( $type =~ s/(?:Single|Multiple)$// ) {
749 $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
752 if ( $FieldTypes{$type} ) {
764 if ($type =~ s/(?:(Single)|Multiple)$//) {
765 $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
766 $self->SetMaxValues($1 ? 1 : 0);
768 $self->_Set(Field => 'Type', Value =>$type);
771 =head2 SetPattern STRING
773 Takes a single string representing a regular expression. Performs basic
774 validation on that regex, and sets the C<Pattern> field for the CF if it
783 my ($ok, $msg) = $self->_IsValidRegex($regex);
785 return $self->_Set(Field => 'Pattern', Value => $regex);
788 return (0, $self->loc("Invalid pattern: [_1]", $msg));
792 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
794 Tests if the string contains an invalid regex.
800 my $regex = shift or return (1, 'valid');
803 local $SIG{__DIE__} = sub { 1 };
804 local $SIG{__WARN__} = sub { 1 };
806 if (eval { qr/$regex/; 1 }) {
811 $err =~ s{[,;].*}{}; # strip debug info from error
819 Returns true if this CustomField only accepts a single value.
820 Returns false if it accepts multiple values
826 if (($self->MaxValues||0) == 1) {
834 sub UnlimitedValues {
836 if (($self->MaxValues||0) == 0) {
845 =head2 CurrentUserHasRight RIGHT
847 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
851 sub CurrentUserHasRight {
855 return $self->CurrentUser->HasRight(
861 =head2 ACLEquivalenceObjects
863 Returns list of objects via which users can get rights on this custom field. For custom fields
864 these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
868 sub ACLEquivalenceObjects {
871 my $ctx = $self->ContextObject
873 return ($ctx, $ctx->ACLEquivalenceObjects);
876 =head2 ContextObject and SetContextObject
878 Set or get a context for this object. It can be ticket, queue or another object
879 this CF applies to. Used for ACL control, for example SeeCustomField can be granted on
880 queue level to allow people to see all fields applied to the queue.
884 sub SetContextObject {
886 return $self->{'context_object'} = shift;
891 return $self->{'context_object'};
898 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
899 return ( 0, $self->loc('Permission Denied') );
901 return $self->SUPER::_Set( @_ );
909 Takes the name of a table column.
910 Returns its value as a string, if the user passes an ACL check
916 return undef unless $self->id;
918 # we need to do the rights check
919 unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
921 "Permission denied. User #". $self->CurrentUser->id
922 ." has no SeeCustomField right on CF #". $self->id
926 return $self->__Value( @_ );
933 1 will cause this custom field to no longer be avaialble for objects.
934 0 will re-enable this field.
939 =head2 SetTypeComposite
941 Set this custom field's type and maximum values as a composite value
945 sub SetTypeComposite {
947 my $composite = shift;
949 my $old = $self->TypeComposite;
951 my ($type, $max_values) = split(/-/, $composite, 2);
952 if ( $type ne $self->Type ) {
953 my ($status, $msg) = $self->SetType( $type );
954 return ($status, $msg) unless $status;
956 if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
957 my ($status, $msg) = $self->SetMaxValues( $max_values );
958 return ($status, $msg) unless $status;
960 my $render = $self->RenderType;
961 if ( $render and not grep { $_ eq $render } $self->RenderTypes ) {
962 # We switched types and our render type is no longer valid, so unset it
963 # and use the default
964 $self->SetRenderType( undef );
966 return 1, $self->loc(
967 "Type changed from '[_1]' to '[_2]'",
968 $self->FriendlyTypeComposite( $old ),
969 $self->FriendlyTypeComposite( $composite ),
975 Returns a composite value composed of this object's type and maximum values
982 return join '-', ($self->Type || ''), ($self->MaxValues || 0);
985 =head2 TypeComposites
987 Returns an array of all possible composite values for custom fields.
993 return grep !/(?:[Tt]ext|Combobox|Date|DateTime|TimeValue)-0/, map { ("$_-1", "$_-0") } $self->Types;
998 Returns the type of form widget to render for this custom field. Currently
999 this only affects fields which return true for L</HasRenderTypes>.
1005 return '' unless $self->HasRenderTypes;
1007 return $self->_Value( 'RenderType', @_ )
1008 || $self->DefaultRenderType;
1011 =head2 SetRenderType TYPE
1013 Sets this custom field's render type.
1020 return (0, $self->loc("This custom field has no Render Types"))
1021 unless $self->HasRenderTypes;
1023 if ( !$type || $type eq $self->DefaultRenderType ) {
1024 return $self->_Set( Field => 'RenderType', Value => undef, @_ );
1027 if ( not grep { $_ eq $type } $self->RenderTypes ) {
1028 return (0, $self->loc("Invalid Render Type for custom field of type [_1]",
1029 $self->FriendlyType));
1032 # XXX: Remove this restriction once we support lists and cascaded selects
1033 if ( $self->BasedOnObj->id and $type =~ /List/ ) {
1034 return (0, $self->loc("We can't currently render as a List when basing categories on another custom field. Please use another render type."));
1037 return $self->_Set( Field => 'RenderType', Value => $type, @_ );
1040 =head2 DefaultRenderType [TYPE COMPOSITE]
1042 Returns the default render type for this custom field's type or the TYPE
1043 COMPOSITE specified as an argument.
1047 sub DefaultRenderType {
1049 my $composite = @_ ? shift : $self->TypeComposite;
1050 my ($type, $max) = split /-/, $composite, 2;
1051 return unless $type and $self->HasRenderTypes($composite);
1052 return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0];
1055 =head2 HasRenderTypes [TYPE_COMPOSITE]
1057 Returns a boolean value indicating whether the L</RenderTypes> and
1058 L</RenderType> methods make sense for this custom field.
1060 Currently true only for type C<Select>.
1064 sub HasRenderTypes {
1066 my ($type, $max) = split /-/, (@_ ? shift : $self->TypeComposite), 2;
1067 return undef unless $type;
1068 return defined $FieldTypes{$type}->{render_types}
1069 ->{ $max == 1 ? 'single' : 'multiple' };
1072 =head2 RenderTypes [TYPE COMPOSITE]
1074 Returns the valid render types for this custom field's type or the TYPE
1075 COMPOSITE specified as an argument.
1081 my $composite = @_ ? shift : $self->TypeComposite;
1082 my ($type, $max) = split /-/, $composite, 2;
1083 return unless $type and $self->HasRenderTypes($composite);
1084 return @{$FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }};
1087 =head2 SetLookupType
1089 Autrijus: care to doc how LookupTypes work?
1096 if ( $lookup ne $self->LookupType ) {
1097 # Okay... We need to invalidate our existing relationships
1098 my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
1099 $ObjectCustomFields->LimitToCustomField($self->Id);
1100 $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
1102 return $self->_Set(Field => 'LookupType', Value =>$lookup);
1107 Returns an array of LookupTypes available
1114 return keys %FRIENDLY_OBJECT_TYPES;
1117 my @FriendlyObjectTypes = (
1118 "[_1] objects", # loc
1119 "[_1]'s [_2] objects", # loc
1120 "[_1]'s [_2]'s [_3] objects", # loc
1123 =head2 FriendlyLookupType
1125 Returns a localized description of the type of this custom field
1129 sub FriendlyLookupType {
1131 my $lookup = shift || $self->LookupType;
1133 return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
1134 if (defined $FRIENDLY_OBJECT_TYPES{$lookup} );
1136 my @types = map { s/^RT::// ? $self->loc($_) : $_ }
1137 grep { defined and length }
1138 split( /-/, $lookup )
1140 return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
1143 sub RecordClassFromLookupType {
1145 my ($class) = ($self->LookupType =~ /^([^-]+)/);
1148 "Custom Field #". $self->id
1149 ." has incorrect LookupType '". $self->LookupType ."'"
1156 sub CollectionClassFromLookupType {
1159 my $record_class = $self->RecordClassFromLookupType;
1160 return undef unless $record_class;
1162 my $collection_class;
1163 if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
1164 $collection_class = $record_class.'Collection';
1165 } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
1166 $collection_class = $record_class.'es';
1167 } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
1168 $collection_class = $record_class.'s';
1170 $RT::Logger->error("Can not find a collection class for record class '$record_class'");
1173 return $collection_class;
1176 =head1 ApplyGlobally
1178 Certain custom fields (users, groups) should only be applied globally
1179 but rather than regexing in code for LookupType =~ RT::Queue, we'll codify
1187 return ($self->LookupType =~ /^RT::(?:Group|User)/io);
1193 Returns collection with objects this custom field is applied to.
1194 Class of the collection depends on L</LookupType>.
1195 See all L</NotAppliedTo> .
1197 Doesn't takes into account if object is applied globally.
1204 my ($res, $ocfs_alias) = $self->_AppliedTo;
1205 return $res unless $res;
1208 ALIAS => $ocfs_alias,
1210 OPERATOR => 'IS NOT',
1219 Returns collection with objects this custom field is not applied to.
1220 Class of the collection depends on L</LookupType>.
1221 See all L</AppliedTo> .
1223 Doesn't takes into account if object is applied globally.
1230 my ($res, $ocfs_alias) = $self->_AppliedTo;
1231 return $res unless $res;
1234 ALIAS => $ocfs_alias,
1246 my ($class) = $self->CollectionClassFromLookupType;
1247 return undef unless $class;
1249 my $res = $class->new( $self->CurrentUser );
1251 # If CF is a Group CF, only display user-defined groups
1252 if ( $class eq 'RT::Groups' ) {
1253 $res->LimitToUserDefinedGroups;
1256 $res->OrderBy( FIELD => 'Name' );
1257 my $ocfs_alias = $res->Join(
1261 TABLE2 => 'ObjectCustomFields',
1262 FIELD2 => 'ObjectId',
1265 LEFTJOIN => $ocfs_alias,
1266 ALIAS => $ocfs_alias,
1267 FIELD => 'CustomField',
1270 return ($res, $ocfs_alias);
1275 Takes object id and returns corresponding L<RT::ObjectCustomField>
1276 record if this custom field is applied to the object. Use 0 to check
1277 if custom field is applied globally.
1284 my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1285 $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1286 return undef unless $ocf->id;
1290 =head2 AddToObject OBJECT
1292 Add this custom field as a custom field for a single object, such as a queue or group.
1302 my $id = $object->Id || 0;
1304 unless (index($self->LookupType, ref($object)) == 0) {
1305 return ( 0, $self->loc('Lookup type mismatch') );
1308 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1309 return ( 0, $self->loc('Permission Denied') );
1312 if ( $self->IsApplied( $id ) ) {
1313 return ( 0, $self->loc("Custom field is already applied to the object") );
1318 return (0, $self->loc("Couldn't apply custom field to an object as it's global already") )
1319 if $self->IsApplied( 0 );
1322 my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
1323 $applied->LimitToCustomField( $self->id );
1324 while ( my $record = $applied->Next ) {
1329 my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1330 my ( $oid, $msg ) = $ocf->Create(
1331 ObjectId => $id, CustomField => $self->id,
1333 return ( $oid, $msg );
1337 =head2 RemoveFromObject OBJECT
1339 Remove this custom field for a single object, such as a queue or group.
1345 sub RemoveFromObject {
1348 my $id = $object->Id || 0;
1350 unless (index($self->LookupType, ref($object)) == 0) {
1351 return ( 0, $self->loc('Object type mismatch') );
1354 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1355 return ( 0, $self->loc('Permission Denied') );
1358 my $ocf = $self->IsApplied( $id );
1360 return ( 0, $self->loc("This custom field does not apply to that object") );
1363 # XXX: Delete doesn't return anything
1364 my ( $oid, $msg ) = $ocf->Delete;
1365 return ( $oid, $msg );
1369 =head2 AddValueForObject HASH
1371 Adds a custom field value for a record object of some kind.
1372 Takes a param hash of
1386 sub AddValueForObject {
1391 LargeContent => undef,
1392 ContentType => undef,
1395 my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1397 unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1398 return ( 0, $self->loc('Permission Denied') );
1401 unless ( $self->MatchPattern($args{'Content'}) ) {
1402 return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1405 $RT::Handle->BeginTransaction;
1407 if ( $self->MaxValues ) {
1408 my $current_values = $self->ValuesForObject($obj);
1409 my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1411 # (The +1 is for the new value we're adding)
1413 # If we have a set of current values and we've gone over the maximum
1414 # allowed number of values, we'll need to delete some to make room.
1415 # which former values are blown away is not guaranteed
1417 while ($extra_values) {
1418 my $extra_item = $current_values->Next;
1419 unless ( $extra_item->id ) {
1420 $RT::Logger->crit( "We were just asked to delete "
1421 ."a custom field value that doesn't exist!" );
1422 $RT::Handle->Rollback();
1425 $extra_item->Delete;
1430 if (my $canonicalizer = $self->can('_CanonicalizeValue'.$self->Type)) {
1431 $canonicalizer->($self, \%args);
1436 my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1437 my ($val, $msg) = $newval->Create(
1438 ObjectType => ref($obj),
1439 ObjectId => $obj->Id,
1440 Content => $args{'Content'},
1441 LargeContent => $args{'LargeContent'},
1442 ContentType => $args{'ContentType'},
1443 CustomField => $self->Id
1447 $RT::Handle->Rollback();
1448 return ($val, $self->loc("Couldn't create record: [_1]", $msg));
1451 $RT::Handle->Commit();
1458 sub _CanonicalizeValueDateTime {
1461 my $DateObj = RT::Date->new( $self->CurrentUser );
1462 $DateObj->Set( Format => 'unknown',
1463 Value => $args->{'Content'} );
1464 $args->{'Content'} = $DateObj->ISO;
1467 # For date, we need to store Content as ISO date
1468 sub _CanonicalizeValueDate {
1472 # in case user input date with time, let's omit it by setting timezone
1473 # to utc so "hour" won't affect "day"
1474 my $DateObj = RT::Date->new( $self->CurrentUser );
1475 $DateObj->Set( Format => 'unknown',
1476 Value => $args->{'Content'},
1479 $args->{'Content'} = $DateObj->Date( Timezone => 'UTC' );
1482 =head2 MatchPattern STRING
1484 Tests the incoming string against the Pattern of this custom field object
1485 and returns a boolean; returns true if the Pattern is empty.
1491 my $regex = $self->Pattern or return 1;
1493 return (( defined $_[0] ? $_[0] : '') =~ $regex);
1499 =head2 FriendlyPattern
1501 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1506 sub FriendlyPattern {
1508 my $regex = $self->Pattern;
1510 return '' unless length $regex;
1511 if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1512 return '[' . $self->loc($1) . ']';
1522 =head2 DeleteValueForObject HASH
1524 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1526 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1530 sub DeleteValueForObject {
1532 my %args = ( Object => undef,
1538 unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1539 return (0, $self->loc('Permission Denied'));
1542 my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1544 if (my $id = $args{'Id'}) {
1547 unless ($oldval->id) {
1548 $oldval->LoadByObjectContentAndCustomField(
1549 Object => $args{'Object'},
1550 Content => $args{'Content'},
1551 CustomField => $self->Id,
1556 # check to make sure we found it
1557 unless ($oldval->Id) {
1558 return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1561 # for single-value fields, we need to validate that empty string is a valid value for it
1562 if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1563 return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1568 my $ret = $oldval->Delete();
1570 return(0, $self->loc("Custom field value could not be found"));
1572 return($oldval->Id, $self->loc("Custom field value deleted"));
1576 =head2 ValuesForObject OBJECT
1578 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT
1582 sub ValuesForObject {
1586 my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
1587 unless ($self->CurrentUserHasRight('SeeCustomField')) {
1588 # Return an empty object if they have no rights to see
1593 $values->LimitToCustomField($self->Id);
1594 $values->LimitToEnabled();
1595 $values->LimitToObject($object);
1601 =head2 _ForObjectType PATH FRIENDLYNAME
1603 Tell RT that a certain object accepts custom fields
1607 'RT::Queue-RT::Ticket' => "Tickets", # loc
1608 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc
1609 'RT::User' => "Users", # loc
1610 'RT::Group' => "Groups", # loc
1612 This is a class method.
1616 sub _ForObjectType {
1619 my $friendly_name = shift;
1621 $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1626 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1628 Gets or sets the C<IncludeContentForValue> for this custom field. RT
1629 uses this field to automatically include content into the user's browser
1630 as they display records with custom fields in RT.
1634 sub SetIncludeContentForValue {
1635 shift->IncludeContentForValue(@_);
1637 sub IncludeContentForValue{
1639 $self->_URLTemplate('IncludeContentForValue', @_);
1644 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1646 Gets or sets the C<LinkValueTo> for this custom field. RT
1647 uses this field to make custom field values into hyperlinks in the user's
1648 browser as they display records with custom fields in RT.
1653 sub SetLinkValueTo {
1654 shift->LinkValueTo(@_);
1659 $self->_URLTemplate('LinkValueTo', @_);
1664 =head2 _URLTemplate NAME [VALUE]
1666 With one argument, returns the _URLTemplate named C<NAME>, but only if
1667 the current user has the right to see this custom field.
1669 With two arguments, attemptes to set the relevant template value.
1675 my $template_name = shift;
1679 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1680 return ( 0, $self->loc('Permission Denied') );
1682 $self->SetAttribute( Name => $template_name, Content => $value );
1683 return ( 1, $self->loc('Updated') );
1685 unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1689 my @attr = $self->Attributes->Named($template_name);
1690 my $attr = shift @attr;
1692 if ($attr) { return $attr->Content }
1701 return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1702 unless defined $value and length $value;
1704 my $cf = RT::CustomField->new( $self->CurrentUser );
1705 $cf->Load( ref $value ? $value->id : $value );
1707 return (0, "Permission denied")
1708 unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
1710 # XXX: Remove this restriction once we support lists and cascaded selects
1711 if ( $self->RenderType =~ /List/ ) {
1712 return (0, $self->loc("We can't currently render as a List when basing categories on another custom field. Please use another render type."));
1715 return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1721 my $obj = RT::CustomField->new( $self->CurrentUser );
1722 if ( $self->BasedOn ) {
1723 $obj->Load( $self->BasedOn );
1730 my $tag = $self->FirstAttribute( 'UILocation' );
1731 return $tag ? $tag->Content : '';
1738 return $self->SetAttribute( Name => 'UILocation', Content => $tag );
1741 return $self->DeleteAttribute('UILocation');
1752 Returns the current value of id.
1753 (In the database, id is stored as int(11).)
1761 Returns the current value of Name.
1762 (In the database, Name is stored as varchar(200).)
1766 =head2 SetName VALUE
1770 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1771 (In the database, Name will be stored as a varchar(200).)
1779 Returns the current value of Type.
1780 (In the database, Type is stored as varchar(200).)
1784 =head2 SetType VALUE
1788 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1789 (In the database, Type will be stored as a varchar(200).)
1797 Returns the current value of RenderType.
1798 (In the database, RenderType is stored as varchar(64).)
1802 =head2 SetRenderType VALUE
1805 Set RenderType to VALUE.
1806 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1807 (In the database, RenderType will be stored as a varchar(64).)
1815 Returns the current value of MaxValues.
1816 (In the database, MaxValues is stored as int(11).)
1820 =head2 SetMaxValues VALUE
1823 Set MaxValues to VALUE.
1824 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1825 (In the database, MaxValues will be stored as a int(11).)
1833 Returns the current value of Pattern.
1834 (In the database, Pattern is stored as text.)
1838 =head2 SetPattern VALUE
1841 Set Pattern to VALUE.
1842 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1843 (In the database, Pattern will be stored as a text.)
1851 Returns the current value of Repeated.
1852 (In the database, Repeated is stored as smallint(6).)
1856 =head2 SetRepeated VALUE
1859 Set Repeated to VALUE.
1860 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1861 (In the database, Repeated will be stored as a smallint(6).)
1869 Returns the current value of BasedOn.
1870 (In the database, BasedOn is stored as int(11).)
1874 =head2 SetBasedOn VALUE
1877 Set BasedOn to VALUE.
1878 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1879 (In the database, BasedOn will be stored as a int(11).)
1887 Returns the current value of Description.
1888 (In the database, Description is stored as varchar(255).)
1892 =head2 SetDescription VALUE
1895 Set Description to VALUE.
1896 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1897 (In the database, Description will be stored as a varchar(255).)
1905 Returns the current value of SortOrder.
1906 (In the database, SortOrder is stored as int(11).)
1910 =head2 SetSortOrder VALUE
1913 Set SortOrder to VALUE.
1914 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1915 (In the database, SortOrder will be stored as a int(11).)
1923 Returns the current value of LookupType.
1924 (In the database, LookupType is stored as varchar(255).)
1928 =head2 SetLookupType VALUE
1931 Set LookupType to VALUE.
1932 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1933 (In the database, LookupType will be stored as a varchar(255).)
1941 Returns the current value of Creator.
1942 (In the database, Creator is stored as int(11).)
1950 Returns the current value of Created.
1951 (In the database, Created is stored as datetime.)
1957 =head2 LastUpdatedBy
1959 Returns the current value of LastUpdatedBy.
1960 (In the database, LastUpdatedBy is stored as int(11).)
1968 Returns the current value of LastUpdated.
1969 (In the database, LastUpdated is stored as datetime.)
1977 Returns the current value of Disabled.
1978 (In the database, Disabled is stored as smallint(6).)
1982 =head2 SetDisabled VALUE
1985 Set Disabled to VALUE.
1986 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1987 (In the database, Disabled will be stored as a smallint(6).)
1994 sub _CoreAccessible {
1998 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2000 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
2002 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
2004 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
2006 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2008 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''},
2010 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
2012 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2014 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
2016 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2018 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
2020 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2022 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
2024 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2026 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
2028 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
2034 RT::Base->_ImportOverlays();