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
177 'Enter multiple time values (UNSUPPORTED)',
178 'Enter a time value',
179 'Enter [_1] time values (UNSUPPORTED)',
187 labels => [ 'Enter multiple IP addresses', # loc
188 'Enter one IP address', # loc
189 'Enter up to [_1] IP addresses', # loc
196 labels => [ 'Enter multiple IP address ranges', # loc
197 'Enter one IP address range', # loc
198 'Enter up to [_1] IP address ranges', # loc
204 our %FRIENDLY_OBJECT_TYPES = ();
206 RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket' => "Tickets", ); #loc
207 RT::CustomField->_ForObjectType(
208 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", ); #loc
209 RT::CustomField->_ForObjectType( 'RT::User' => "Users", ); #loc
210 RT::CustomField->_ForObjectType( 'RT::Queue' => "Queues", ); #loc
211 RT::CustomField->_ForObjectType( 'RT::Group' => "Groups", ); #loc
214 SeeCustomField => 'View custom fields', # loc_pair
215 AdminCustomField => 'Create, modify and delete custom fields', # loc_pair
216 AdminCustomFieldValues => 'Create, modify and delete custom fields values', # loc_pair
217 ModifyCustomField => 'Add, modify and delete custom field values for objects' # loc_pair
220 our $RIGHT_CATEGORIES = {
221 SeeCustomField => 'General',
222 AdminCustomField => 'Admin',
223 AdminCustomFieldValues => 'Admin',
224 ModifyCustomField => 'Staff',
227 # Tell RT::ACE that this sort of object can get acls granted
228 $RT::ACE::OBJECT_TYPES{'RT::CustomField'} = 1;
230 __PACKAGE__->AddRights(%$RIGHTS);
231 __PACKAGE__->AddRightCategories(%$RIGHT_CATEGORIES);
233 =head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...]
235 Adds the given rights to the list of possible rights. This method
236 should be called during server startup, not at runtime.
243 $RIGHTS = { %$RIGHTS, %new };
244 %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES,
245 map { lc($_) => $_ } keys %new);
248 sub AvailableRights {
253 =head2 RightCategories
255 Returns a hashref where the keys are rights for this type of object and the
256 values are the category (General, Staff, Admin) the right falls into.
260 sub RightCategories {
261 return $RIGHT_CATEGORIES;
264 =head2 AddRightCategories C<RIGHT>, C<CATEGORY> [, ...]
266 Adds the given right and category pairs to the list of right categories. This
267 method should be called during server startup, not at runtime.
271 sub AddRightCategories {
272 my $self = shift if ref $_[0] or $_[0] eq __PACKAGE__;
274 $RIGHT_CATEGORIES = { %$RIGHT_CATEGORIES, %new };
279 RT::CustomField_Overlay - overlay for RT::CustomField
283 =head1 'CORE' METHODS
285 =head2 Create PARAMHASH
287 Create takes a hash of values and creates a row in the database:
292 varchar(255) 'Pattern'.
293 smallint(6) 'Repeated'.
294 varchar(255) 'Description'.
296 varchar(255) 'LookupType'.
297 smallint(6) 'Disabled'.
299 C<LookupType> is generally the result of either
300 C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType>.
316 IncludeContentForValue => '',
320 unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) {
321 return (0, $self->loc('Permission Denied'));
324 if ( $args{TypeComposite} ) {
325 @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
327 elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) {
328 # old style Type string
329 $args{'MaxValues'} = $1 ? 1 : 0;
331 $args{'MaxValues'} = int $args{'MaxValues'};
333 if ( !exists $args{'Queue'}) {
334 # do nothing -- things below are strictly backward compat
336 elsif ( ! $args{'Queue'} ) {
337 unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
338 return ( 0, $self->loc('Permission Denied') );
340 $args{'LookupType'} = 'RT::Queue-RT::Ticket';
343 my $queue = RT::Queue->new($self->CurrentUser);
344 $queue->Load($args{'Queue'});
345 unless ($queue->Id) {
346 return (0, $self->loc("Queue not found"));
348 unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
349 return ( 0, $self->loc('Permission Denied') );
351 $args{'LookupType'} = 'RT::Queue-RT::Ticket';
352 $args{'Queue'} = $queue->Id;
355 my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} );
356 return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok;
358 if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) {
359 $RT::Logger->debug("Support for 'multiple' Texts or Comboboxes is not implemented");
360 $args{'MaxValues'} = 1;
363 if ( $args{'RenderType'} ||= undef ) {
364 my $composite = join '-', @args{'Type', 'MaxValues'};
365 return (0, $self->loc("This custom field has no Render Types"))
366 unless $self->HasRenderTypes( $composite );
368 if ( $args{'RenderType'} eq $self->DefaultRenderType( $composite ) ) {
369 $args{'RenderType'} = undef;
371 return (0, $self->loc("Invalid Render Type") )
372 unless grep $_ eq $args{'RenderType'}, $self->RenderTypes( $composite );
376 $args{'ValuesClass'} = undef if ($args{'ValuesClass'} || '') eq 'RT::CustomFieldValues';
377 if ( $args{'ValuesClass'} ||= undef ) {
378 return (0, $self->loc("This Custom Field can not have list of values"))
379 unless $self->IsSelectionType( $args{'Type'} );
381 unless ( $self->ValidateValuesClass( $args{'ValuesClass'} ) ) {
382 return (0, $self->loc("Invalid Custom Field values source"));
386 (my $rv, $msg) = $self->SUPER::Create(
387 Name => $args{'Name'},
388 Type => $args{'Type'},
389 RenderType => $args{'RenderType'},
390 MaxValues => $args{'MaxValues'},
391 Pattern => $args{'Pattern'},
392 BasedOn => $args{'BasedOn'},
393 ValuesClass => $args{'ValuesClass'},
394 Description => $args{'Description'},
395 Disabled => $args{'Disabled'},
396 LookupType => $args{'LookupType'},
397 Repeated => $args{'Repeated'},
401 if ( exists $args{'LinkValueTo'}) {
402 $self->SetLinkValueTo($args{'LinkValueTo'});
405 if ( exists $args{'IncludeContentForValue'}) {
406 $self->SetIncludeContentForValue($args{'IncludeContentForValue'});
409 if ( exists $args{'UILocation'} ) {
410 $self->SetUILocation( $args{'UILocation'} );
413 return ($rv, $msg) unless exists $args{'Queue'};
415 # Compat code -- create a new ObjectCustomField mapping
416 my $OCF = RT::ObjectCustomField->new( $self->CurrentUser );
418 CustomField => $self->Id,
419 ObjectId => $args{'Queue'},
428 Load a custom field. If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
434 my $id = shift || '';
436 if ( $id =~ /^\d+$/ ) {
437 return $self->SUPER::Load( $id );
439 return $self->LoadByName( Name => $id );
445 =head2 LoadByName (Queue => QUEUEID, Name => NAME)
447 Loads the Custom field named NAME.
449 Will load a Disabled Custom Field even if there is a non-disabled Custom Field
452 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
454 If the Queue parameter is '0', look for global ticket custom fields.
456 If no queue parameter is specified, look for any and all custom fields with this name.
458 BUG/TODO, this won't let you specify that you only want user or group CFs.
462 # Compatibility for API change after 3.0 beta 1
463 *LoadNameAndQueue = \&LoadByName;
464 # Change after 3.4 beta.
465 *LoadByNameAndQueue = \&LoadByName;
475 unless ( defined $args{'Name'} && length $args{'Name'} ) {
476 $RT::Logger->error("Couldn't load Custom Field without Name");
477 return wantarray ? (0, $self->loc("No name provided")) : 0;
480 # if we're looking for a queue by name, make it a number
481 if ( defined $args{'Queue'} && ($args{'Queue'} =~ /\D/ || !$self->ContextObject) ) {
482 my $QueueObj = RT::Queue->new( $self->CurrentUser );
483 $QueueObj->Load( $args{'Queue'} );
484 $args{'Queue'} = $QueueObj->Id;
485 $self->SetContextObject( $QueueObj )
486 unless $self->ContextObject;
489 # XXX - really naive implementation. Slow. - not really. still just one query
491 my $CFs = RT::CustomFields->new( $self->CurrentUser );
492 $CFs->SetContextObject( $self->ContextObject );
493 my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
494 $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
495 # Don't limit to queue if queue is 0. Trying to do so breaks
496 # RT::Group type CFs.
497 if ( defined $args{'Queue'} ) {
498 $CFs->LimitToQueue( $args{'Queue'} );
501 # When loading by name, we _can_ load disabled fields, but prefer
502 # non-disabled fields.
505 { FIELD => "Disabled", ORDER => 'ASC' },
508 # We only want one entry.
509 $CFs->RowsPerPage(1);
511 # version before 3.8 just returns 0, so we need to test if wantarray to be
512 # backward compatible.
513 return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
515 return $self->LoadById( $first->id );
521 =head2 Custom field values
525 Return a object (collection) of all acceptable values for this Custom Field.
526 Class of the object can vary and depends on the return value
527 of the C<ValuesClass> method.
531 *ValuesObj = \&Values;
536 my $class = $self->ValuesClass;
537 if ( $class ne 'RT::CustomFieldValues') {
538 eval "require $class" or die "$@";
540 my $cf_values = $class->new( $self->CurrentUser );
541 # if the user has no rights, return an empty object
542 if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
543 $cf_values->LimitToCustomField( $self->Id );
545 $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
553 Create a new value for this CustomField. Takes a paramhash containing the elements Name, Description and SortOrder
561 unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) {
562 return (0, $self->loc('Permission Denied'));
566 if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
567 return (0, $self->loc("Can't add a custom field value without a name"));
570 my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
571 return $newval->Create( %args, CustomField => $self->Id );
577 =head3 DeleteValue ID
579 Deletes a value from this custom field by id.
581 Does not remove this value for any article which has had it selected
588 unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) {
589 return (0, $self->loc('Permission Denied'));
592 my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
593 $val_to_del->Load( $id );
594 unless ( $val_to_del->Id ) {
595 return (0, $self->loc("Couldn't find that value"));
597 unless ( $val_to_del->CustomField == $self->Id ) {
598 return (0, $self->loc("That is not a value for this custom field"));
601 my $retval = $val_to_del->Delete;
603 return (0, $self->loc("Custom field value could not be deleted"));
605 return ($retval, $self->loc("Custom field value deleted"));
609 =head2 ValidateQueue Queue
611 Make sure that the name specified is valid
619 return 0 unless length $value;
621 return $self->SUPER::ValidateName($value);
624 =head2 ValidateQueue Queue
626 Make sure that the queue specified is a valid queue name
634 return undef unless defined $id;
635 # 0 means "Global" null would _not_ be ok.
636 return 1 if $id eq '0';
638 my $q = RT::Queue->new( RT->SystemUser );
640 return undef unless $q->id;
648 Retuns an array of the types of CustomField that are supported
653 return (sort {(($FieldTypes{$a}{sort_order}||999) <=> ($FieldTypes{$b}{sort_order}||999)) or ($a cmp $b)} keys %FieldTypes);
657 =head2 IsSelectionType
659 Retuns a boolean value indicating whether the C<Values> method makes sense
660 to this Custom Field.
664 sub IsSelectionType {
666 my $type = @_? shift : $self->Type;
667 return undef unless $type;
668 return $FieldTypes{$type}->{selection_type};
673 =head2 IsExternalValues
677 sub IsExternalValues {
679 return 0 unless $self->IsSelectionType( @_ );
680 return $self->ValuesClass eq 'RT::CustomFieldValues'? 0 : 1;
685 return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues';
690 my $class = shift || 'RT::CustomFieldValues';
692 if ( $class eq 'RT::CustomFieldValues' ) {
693 return $self->_Set( Field => 'ValuesClass', Value => undef, @_ );
696 return (0, $self->loc("This Custom Field can not have list of values"))
697 unless $self->IsSelectionType;
699 unless ( $self->ValidateValuesClass( $class ) ) {
700 return (0, $self->loc("Invalid Custom Field values source"));
702 return $self->_Set( Field => 'ValuesClass', Value => $class, @_ );
705 sub ValidateValuesClass {
709 return 1 if !defined $class || $class eq 'RT::CustomFieldValues';
710 return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesSources');
715 =head2 FriendlyType [TYPE, MAX_VALUES]
717 Returns a localized human-readable version of the custom field type.
718 If a custom field type is specified as the parameter, the friendly type for that type will be returned
725 my $type = @_ ? shift : $self->Type;
726 my $max = @_ ? shift : $self->MaxValues;
727 $max = 0 unless $max;
729 if (my $friendly_type = $FieldTypes{$type}->{labels}->[$max>2 ? 2 : $max]) {
730 return ( $self->loc( $friendly_type, $max ) );
733 return ( $self->loc( $type ) );
737 sub FriendlyTypeComposite {
739 my $composite = shift || $self->TypeComposite;
740 return $self->FriendlyType(split(/-/, $composite, 2));
744 =head2 ValidateType TYPE
746 Takes a single string. returns true if that string is a value
756 if ( $type =~ s/(?:Single|Multiple)$// ) {
757 $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
760 if ( $FieldTypes{$type} ) {
772 if ($type =~ s/(?:(Single)|Multiple)$//) {
773 $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
774 $self->SetMaxValues($1 ? 1 : 0);
776 $self->_Set(Field => 'Type', Value =>$type);
779 =head2 SetPattern STRING
781 Takes a single string representing a regular expression. Performs basic
782 validation on that regex, and sets the C<Pattern> field for the CF if it
791 my ($ok, $msg) = $self->_IsValidRegex($regex);
793 return $self->_Set(Field => 'Pattern', Value => $regex);
796 return (0, $self->loc("Invalid pattern: [_1]", $msg));
800 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
802 Tests if the string contains an invalid regex.
808 my $regex = shift or return (1, 'valid');
811 local $SIG{__DIE__} = sub { 1 };
812 local $SIG{__WARN__} = sub { 1 };
814 if (eval { qr/$regex/; 1 }) {
819 $err =~ s{[,;].*}{}; # strip debug info from error
827 Returns true if this CustomField only accepts a single value.
828 Returns false if it accepts multiple values
834 if (($self->MaxValues||0) == 1) {
842 sub UnlimitedValues {
844 if (($self->MaxValues||0) == 0) {
853 =head2 CurrentUserHasRight RIGHT
855 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
859 sub CurrentUserHasRight {
863 return $self->CurrentUser->HasRight(
869 =head2 ACLEquivalenceObjects
871 Returns list of objects via which users can get rights on this custom field. For custom fields
872 these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
876 sub ACLEquivalenceObjects {
879 my $ctx = $self->ContextObject
881 return ($ctx, $ctx->ACLEquivalenceObjects);
884 =head2 ContextObject and SetContextObject
886 Set or get a context for this object. It can be ticket, queue or another object
887 this CF applies to. Used for ACL control, for example SeeCustomField can be granted on
888 queue level to allow people to see all fields applied to the queue.
892 sub SetContextObject {
894 return $self->{'context_object'} = shift;
899 return $self->{'context_object'};
902 sub ValidContextType {
907 $valid{$_}++ for split '-', $self->LookupType;
908 delete $valid{'RT::Transaction'};
910 return $valid{$class};
913 =head2 LoadContextObject
915 Takes an Id for a Context Object and loads the right kind of RT::Object
916 for this particular Custom Field (based on the LookupType) and returns it.
917 This is a good way to ensure you don't try to use a Queue as a Context
918 Object on a User Custom Field.
922 sub LoadContextObject {
925 my $contextid = shift;
927 unless ( $self->ValidContextType($type) ) {
928 RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id);
932 my $context_object = $type->new( $self->CurrentUser );
933 my ($id, $msg) = $context_object->LoadById( $contextid );
935 RT->Logger->debug("Invalid ContextObject id: $msg");
938 return $context_object;
941 =head2 ValidateContextObject
943 Ensure that a given ContextObject applies to this Custom Field.
944 For custom fields that are assigned to Queues or to Classes, this checks that the Custom
945 Field is actually applied to that objects. For Global Custom Fields, it returns true
946 as long as the Object is of the right type, because you may be using
947 your permissions on a given Queue of Class to see a Global CF.
948 For CFs that are only applied Globally, you don't need a ContextObject.
952 sub ValidateContextObject {
956 return 1 if $self->IsApplied(0);
958 # global only custom fields don't have objects
959 # that should be used as context objects.
960 return if $self->ApplyGlobally;
962 # Otherwise, make sure we weren't passed a user object that we're
963 # supposed to treat as a queue.
964 return unless $self->ValidContextType(ref $object);
966 # Check that it is applied correctly
967 my ($applied_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects);
968 return unless $applied_to;
969 return $self->IsApplied($applied_to->id);
976 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
977 return ( 0, $self->loc('Permission Denied') );
979 return $self->SUPER::_Set( @_ );
987 Takes the name of a table column.
988 Returns its value as a string, if the user passes an ACL check
994 return undef unless $self->id;
996 # we need to do the rights check
997 unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
999 "Permission denied. User #". $self->CurrentUser->id
1000 ." has no SeeCustomField right on CF #". $self->id
1004 return $self->__Value( @_ );
1011 1 will cause this custom field to no longer be avaialble for objects.
1012 0 will re-enable this field.
1017 =head2 SetTypeComposite
1019 Set this custom field's type and maximum values as a composite value
1023 sub SetTypeComposite {
1025 my $composite = shift;
1027 my $old = $self->TypeComposite;
1029 my ($type, $max_values) = split(/-/, $composite, 2);
1030 if ( $type ne $self->Type ) {
1031 my ($status, $msg) = $self->SetType( $type );
1032 return ($status, $msg) unless $status;
1034 if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
1035 my ($status, $msg) = $self->SetMaxValues( $max_values );
1036 return ($status, $msg) unless $status;
1038 my $render = $self->RenderType;
1039 if ( $render and not grep { $_ eq $render } $self->RenderTypes ) {
1040 # We switched types and our render type is no longer valid, so unset it
1041 # and use the default
1042 $self->SetRenderType( undef );
1044 return 1, $self->loc(
1045 "Type changed from '[_1]' to '[_2]'",
1046 $self->FriendlyTypeComposite( $old ),
1047 $self->FriendlyTypeComposite( $composite ),
1051 =head2 TypeComposite
1053 Returns a composite value composed of this object's type and maximum values
1060 return join '-', ($self->Type || ''), ($self->MaxValues || 0);
1063 =head2 TypeComposites
1065 Returns an array of all possible composite values for custom fields.
1069 sub TypeComposites {
1071 return grep !/(?:[Tt]ext|Combobox|Date|DateTime|TimeValue)-0/, map { ("$_-1", "$_-0") } $self->Types;
1076 Returns the type of form widget to render for this custom field. Currently
1077 this only affects fields which return true for L</HasRenderTypes>.
1083 return '' unless $self->HasRenderTypes;
1085 return $self->_Value( 'RenderType', @_ )
1086 || $self->DefaultRenderType;
1089 =head2 SetRenderType TYPE
1091 Sets this custom field's render type.
1098 return (0, $self->loc("This custom field has no Render Types"))
1099 unless $self->HasRenderTypes;
1101 if ( !$type || $type eq $self->DefaultRenderType ) {
1102 return $self->_Set( Field => 'RenderType', Value => undef, @_ );
1105 if ( not grep { $_ eq $type } $self->RenderTypes ) {
1106 return (0, $self->loc("Invalid Render Type for custom field of type [_1]",
1107 $self->FriendlyType));
1110 # XXX: Remove this restriction once we support lists and cascaded selects
1111 if ( $self->BasedOnObj->id and $type =~ /List/ ) {
1112 return (0, $self->loc("We can't currently render as a List when basing categories on another custom field. Please use another render type."));
1115 return $self->_Set( Field => 'RenderType', Value => $type, @_ );
1118 =head2 DefaultRenderType [TYPE COMPOSITE]
1120 Returns the default render type for this custom field's type or the TYPE
1121 COMPOSITE specified as an argument.
1125 sub DefaultRenderType {
1127 my $composite = @_ ? shift : $self->TypeComposite;
1128 my ($type, $max) = split /-/, $composite, 2;
1129 return unless $type and $self->HasRenderTypes($composite);
1130 return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0];
1133 =head2 HasRenderTypes [TYPE_COMPOSITE]
1135 Returns a boolean value indicating whether the L</RenderTypes> and
1136 L</RenderType> methods make sense for this custom field.
1138 Currently true only for type C<Select>.
1142 sub HasRenderTypes {
1144 my ($type, $max) = split /-/, (@_ ? shift : $self->TypeComposite), 2;
1145 return undef unless $type;
1146 return defined $FieldTypes{$type}->{render_types}
1147 ->{ $max == 1 ? 'single' : 'multiple' };
1150 =head2 RenderTypes [TYPE COMPOSITE]
1152 Returns the valid render types for this custom field's type or the TYPE
1153 COMPOSITE specified as an argument.
1159 my $composite = @_ ? shift : $self->TypeComposite;
1160 my ($type, $max) = split /-/, $composite, 2;
1161 return unless $type and $self->HasRenderTypes($composite);
1162 return @{$FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }};
1165 =head2 SetLookupType
1167 Autrijus: care to doc how LookupTypes work?
1174 if ( $lookup ne $self->LookupType ) {
1175 # Okay... We need to invalidate our existing relationships
1176 my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
1177 $ObjectCustomFields->LimitToCustomField($self->Id);
1178 $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
1180 return $self->_Set(Field => 'LookupType', Value =>$lookup);
1185 Returns an array of LookupTypes available
1192 return keys %FRIENDLY_OBJECT_TYPES;
1195 my @FriendlyObjectTypes = (
1196 "[_1] objects", # loc
1197 "[_1]'s [_2] objects", # loc
1198 "[_1]'s [_2]'s [_3] objects", # loc
1201 =head2 FriendlyLookupType
1203 Returns a localized description of the type of this custom field
1207 sub FriendlyLookupType {
1209 my $lookup = shift || $self->LookupType;
1211 return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
1212 if (defined $FRIENDLY_OBJECT_TYPES{$lookup} );
1214 my @types = map { s/^RT::// ? $self->loc($_) : $_ }
1215 grep { defined and length }
1216 split( /-/, $lookup )
1218 return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
1221 sub RecordClassFromLookupType {
1223 my ($class) = ($self->LookupType =~ /^([^-]+)/);
1226 "Custom Field #". $self->id
1227 ." has incorrect LookupType '". $self->LookupType ."'"
1234 sub CollectionClassFromLookupType {
1237 my $record_class = $self->RecordClassFromLookupType;
1238 return undef unless $record_class;
1240 my $collection_class;
1241 if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
1242 $collection_class = $record_class.'Collection';
1243 } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
1244 $collection_class = $record_class.'es';
1245 } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
1246 $collection_class = $record_class.'s';
1248 $RT::Logger->error("Can not find a collection class for record class '$record_class'");
1251 return $collection_class;
1254 =head1 ApplyGlobally
1256 Certain custom fields (users, groups) should only be applied globally
1257 but rather than regexing in code for LookupType =~ RT::Queue, we'll codify
1265 return ($self->LookupType =~ /^RT::(?:Group|User)/io);
1271 Returns collection with objects this custom field is applied to.
1272 Class of the collection depends on L</LookupType>.
1273 See all L</NotAppliedTo> .
1275 Doesn't takes into account if object is applied globally.
1282 my ($res, $ocfs_alias) = $self->_AppliedTo;
1283 return $res unless $res;
1286 ALIAS => $ocfs_alias,
1288 OPERATOR => 'IS NOT',
1297 Returns collection with objects this custom field is not applied to.
1298 Class of the collection depends on L</LookupType>.
1299 See all L</AppliedTo> .
1301 Doesn't takes into account if object is applied globally.
1308 my ($res, $ocfs_alias) = $self->_AppliedTo;
1309 return $res unless $res;
1312 ALIAS => $ocfs_alias,
1324 my ($class) = $self->CollectionClassFromLookupType;
1325 return undef unless $class;
1327 my $res = $class->new( $self->CurrentUser );
1329 # If CF is a Group CF, only display user-defined groups
1330 if ( $class eq 'RT::Groups' ) {
1331 $res->LimitToUserDefinedGroups;
1334 $res->OrderBy( FIELD => 'Name' );
1335 my $ocfs_alias = $res->Join(
1339 TABLE2 => 'ObjectCustomFields',
1340 FIELD2 => 'ObjectId',
1343 LEFTJOIN => $ocfs_alias,
1344 ALIAS => $ocfs_alias,
1345 FIELD => 'CustomField',
1348 return ($res, $ocfs_alias);
1353 Takes object id and returns corresponding L<RT::ObjectCustomField>
1354 record if this custom field is applied to the object. Use 0 to check
1355 if custom field is applied globally.
1362 my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1363 $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1364 return undef unless $ocf->id;
1368 =head2 AddToObject OBJECT
1370 Add this custom field as a custom field for a single object, such as a queue or group.
1380 my $id = $object->Id || 0;
1382 unless (index($self->LookupType, ref($object)) == 0) {
1383 return ( 0, $self->loc('Lookup type mismatch') );
1386 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1387 return ( 0, $self->loc('Permission Denied') );
1390 if ( $self->IsApplied( $id ) ) {
1391 return ( 0, $self->loc("Custom field is already applied to the object") );
1396 return (0, $self->loc("Couldn't apply custom field to an object as it's global already") )
1397 if $self->IsApplied( 0 );
1400 my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
1401 $applied->LimitToCustomField( $self->id );
1402 while ( my $record = $applied->Next ) {
1407 my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1408 my ( $oid, $msg ) = $ocf->Create(
1409 ObjectId => $id, CustomField => $self->id,
1411 return ( $oid, $msg );
1415 =head2 RemoveFromObject OBJECT
1417 Remove this custom field for a single object, such as a queue or group.
1423 sub RemoveFromObject {
1426 my $id = $object->Id || 0;
1428 unless (index($self->LookupType, ref($object)) == 0) {
1429 return ( 0, $self->loc('Object type mismatch') );
1432 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1433 return ( 0, $self->loc('Permission Denied') );
1436 my $ocf = $self->IsApplied( $id );
1438 return ( 0, $self->loc("This custom field does not apply to that object") );
1441 # XXX: Delete doesn't return anything
1442 my ( $oid, $msg ) = $ocf->Delete;
1443 return ( $oid, $msg );
1447 =head2 AddValueForObject HASH
1449 Adds a custom field value for a record object of some kind.
1450 Takes a param hash of
1464 sub AddValueForObject {
1469 LargeContent => undef,
1470 ContentType => undef,
1473 my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1475 unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1476 return ( 0, $self->loc('Permission Denied') );
1479 unless ( $self->MatchPattern($args{'Content'}) ) {
1480 return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1483 $RT::Handle->BeginTransaction;
1485 if ( $self->MaxValues ) {
1486 my $current_values = $self->ValuesForObject($obj);
1487 my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1489 # (The +1 is for the new value we're adding)
1491 # If we have a set of current values and we've gone over the maximum
1492 # allowed number of values, we'll need to delete some to make room.
1493 # which former values are blown away is not guaranteed
1495 while ($extra_values) {
1496 my $extra_item = $current_values->Next;
1497 unless ( $extra_item->id ) {
1498 $RT::Logger->crit( "We were just asked to delete "
1499 ."a custom field value that doesn't exist!" );
1500 $RT::Handle->Rollback();
1503 $extra_item->Delete;
1508 if (my $canonicalizer = $self->can('_CanonicalizeValue'.$self->Type)) {
1509 $canonicalizer->($self, \%args);
1514 my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1515 my ($val, $msg) = $newval->Create(
1516 ObjectType => ref($obj),
1517 ObjectId => $obj->Id,
1518 Content => $args{'Content'},
1519 LargeContent => $args{'LargeContent'},
1520 ContentType => $args{'ContentType'},
1521 CustomField => $self->Id
1525 $RT::Handle->Rollback();
1526 return ($val, $self->loc("Couldn't create record: [_1]", $msg));
1529 $RT::Handle->Commit();
1536 sub _CanonicalizeValueDateTime {
1539 my $DateObj = RT::Date->new( $self->CurrentUser );
1540 $DateObj->Set( Format => 'unknown',
1541 Value => $args->{'Content'} );
1542 $args->{'Content'} = $DateObj->ISO;
1545 # For date, we need to store Content as ISO date
1546 sub _CanonicalizeValueDate {
1550 # in case user input date with time, let's omit it by setting timezone
1551 # to utc so "hour" won't affect "day"
1552 my $DateObj = RT::Date->new( $self->CurrentUser );
1553 $DateObj->Set( Format => 'unknown',
1554 Value => $args->{'Content'},
1557 $args->{'Content'} = $DateObj->Date( Timezone => 'UTC' );
1560 =head2 MatchPattern STRING
1562 Tests the incoming string against the Pattern of this custom field object
1563 and returns a boolean; returns true if the Pattern is empty.
1569 my $regex = $self->Pattern or return 1;
1571 return (( defined $_[0] ? $_[0] : '') =~ $regex);
1577 =head2 FriendlyPattern
1579 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1584 sub FriendlyPattern {
1586 my $regex = $self->Pattern;
1588 return '' unless length $regex;
1589 if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1590 return '[' . $self->loc($1) . ']';
1600 =head2 DeleteValueForObject HASH
1602 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1604 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1608 sub DeleteValueForObject {
1610 my %args = ( Object => undef,
1616 unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1617 return (0, $self->loc('Permission Denied'));
1620 my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1622 if (my $id = $args{'Id'}) {
1625 unless ($oldval->id) {
1626 $oldval->LoadByObjectContentAndCustomField(
1627 Object => $args{'Object'},
1628 Content => $args{'Content'},
1629 CustomField => $self->Id,
1634 # check to make sure we found it
1635 unless ($oldval->Id) {
1636 return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1639 # for single-value fields, we need to validate that empty string is a valid value for it
1640 if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1641 return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1646 my $ret = $oldval->Delete();
1648 return(0, $self->loc("Custom field value could not be found"));
1650 return($oldval->Id, $self->loc("Custom field value deleted"));
1654 =head2 ValuesForObject OBJECT
1656 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT
1660 sub ValuesForObject {
1664 my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
1665 unless ($self->CurrentUserHasRight('SeeCustomField')) {
1666 # Return an empty object if they have no rights to see
1671 $values->LimitToCustomField($self->Id);
1672 $values->LimitToEnabled();
1673 $values->LimitToObject($object);
1679 =head2 _ForObjectType PATH FRIENDLYNAME
1681 Tell RT that a certain object accepts custom fields
1685 'RT::Queue-RT::Ticket' => "Tickets", # loc
1686 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc
1687 'RT::User' => "Users", # loc
1688 'RT::Group' => "Groups", # loc
1690 This is a class method.
1694 sub _ForObjectType {
1697 my $friendly_name = shift;
1699 $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1704 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1706 Gets or sets the C<IncludeContentForValue> for this custom field. RT
1707 uses this field to automatically include content into the user's browser
1708 as they display records with custom fields in RT.
1712 sub SetIncludeContentForValue {
1713 shift->IncludeContentForValue(@_);
1715 sub IncludeContentForValue{
1717 $self->_URLTemplate('IncludeContentForValue', @_);
1722 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1724 Gets or sets the C<LinkValueTo> for this custom field. RT
1725 uses this field to make custom field values into hyperlinks in the user's
1726 browser as they display records with custom fields in RT.
1731 sub SetLinkValueTo {
1732 shift->LinkValueTo(@_);
1737 $self->_URLTemplate('LinkValueTo', @_);
1742 =head2 _URLTemplate NAME [VALUE]
1744 With one argument, returns the _URLTemplate named C<NAME>, but only if
1745 the current user has the right to see this custom field.
1747 With two arguments, attemptes to set the relevant template value.
1753 my $template_name = shift;
1757 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1758 return ( 0, $self->loc('Permission Denied') );
1760 $self->SetAttribute( Name => $template_name, Content => $value );
1761 return ( 1, $self->loc('Updated') );
1763 unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1767 my @attr = $self->Attributes->Named($template_name);
1768 my $attr = shift @attr;
1770 if ($attr) { return $attr->Content }
1779 return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1780 unless defined $value and length $value;
1782 my $cf = RT::CustomField->new( $self->CurrentUser );
1783 $cf->SetContextObject( $self->ContextObject );
1784 $cf->Load( ref $value ? $value->id : $value );
1786 return (0, "Permission denied")
1787 unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
1789 # XXX: Remove this restriction once we support lists and cascaded selects
1790 if ( $self->RenderType =~ /List/ ) {
1791 return (0, $self->loc("We can't currently render as a List when basing categories on another custom field. Please use another render type."));
1794 return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1800 my $obj = RT::CustomField->new( $self->CurrentUser );
1801 $obj->SetContextObject( $self->ContextObject );
1802 if ( $self->BasedOn ) {
1803 $obj->Load( $self->BasedOn );
1810 my $tag = $self->FirstAttribute( 'UILocation' );
1811 return $tag ? $tag->Content : '';
1818 return $self->SetAttribute( Name => 'UILocation', Content => $tag );
1821 return $self->DeleteAttribute('UILocation');
1832 Returns the current value of id.
1833 (In the database, id is stored as int(11).)
1841 Returns the current value of Name.
1842 (In the database, Name is stored as varchar(200).)
1846 =head2 SetName VALUE
1850 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1851 (In the database, Name will be stored as a varchar(200).)
1859 Returns the current value of Type.
1860 (In the database, Type is stored as varchar(200).)
1864 =head2 SetType VALUE
1868 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1869 (In the database, Type will be stored as a varchar(200).)
1877 Returns the current value of RenderType.
1878 (In the database, RenderType is stored as varchar(64).)
1882 =head2 SetRenderType VALUE
1885 Set RenderType to VALUE.
1886 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1887 (In the database, RenderType will be stored as a varchar(64).)
1895 Returns the current value of MaxValues.
1896 (In the database, MaxValues is stored as int(11).)
1900 =head2 SetMaxValues VALUE
1903 Set MaxValues to VALUE.
1904 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1905 (In the database, MaxValues will be stored as a int(11).)
1913 Returns the current value of Pattern.
1914 (In the database, Pattern is stored as text.)
1918 =head2 SetPattern VALUE
1921 Set Pattern to VALUE.
1922 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1923 (In the database, Pattern will be stored as a text.)
1931 Returns the current value of Repeated.
1932 (In the database, Repeated is stored as smallint(6).)
1936 =head2 SetRepeated VALUE
1939 Set Repeated to VALUE.
1940 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1941 (In the database, Repeated will be stored as a smallint(6).)
1949 Returns the current value of BasedOn.
1950 (In the database, BasedOn is stored as int(11).)
1954 =head2 SetBasedOn VALUE
1957 Set BasedOn to VALUE.
1958 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1959 (In the database, BasedOn will be stored as a int(11).)
1967 Returns the current value of Description.
1968 (In the database, Description is stored as varchar(255).)
1972 =head2 SetDescription VALUE
1975 Set Description to VALUE.
1976 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1977 (In the database, Description will be stored as a varchar(255).)
1985 Returns the current value of SortOrder.
1986 (In the database, SortOrder is stored as int(11).)
1990 =head2 SetSortOrder VALUE
1993 Set SortOrder to VALUE.
1994 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1995 (In the database, SortOrder will be stored as a int(11).)
2003 Returns the current value of LookupType.
2004 (In the database, LookupType is stored as varchar(255).)
2008 =head2 SetLookupType VALUE
2011 Set LookupType to VALUE.
2012 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2013 (In the database, LookupType will be stored as a varchar(255).)
2021 Returns the current value of Creator.
2022 (In the database, Creator is stored as int(11).)
2030 Returns the current value of Created.
2031 (In the database, Created is stored as datetime.)
2037 =head2 LastUpdatedBy
2039 Returns the current value of LastUpdatedBy.
2040 (In the database, LastUpdatedBy is stored as int(11).)
2048 Returns the current value of LastUpdated.
2049 (In the database, LastUpdated is stored as datetime.)
2057 Returns the current value of Disabled.
2058 (In the database, Disabled is stored as smallint(6).)
2062 =head2 SetDisabled VALUE
2065 Set Disabled to VALUE.
2066 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2067 (In the database, Disabled will be stored as a smallint(6).)
2074 sub _CoreAccessible {
2078 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2080 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
2082 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
2084 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
2086 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2088 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''},
2090 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
2092 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2094 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
2096 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2098 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', 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, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2106 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
2108 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
2114 RT::Base->_ImportOverlays();