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;
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 if ( exists $args{'NoClone'} ) {
414 $self->SetNoClone( $args{'NoClone'} );
417 return ($rv, $msg) unless exists $args{'Queue'};
419 # Compat code -- create a new ObjectCustomField mapping
420 my $OCF = RT::ObjectCustomField->new( $self->CurrentUser );
422 CustomField => $self->Id,
423 ObjectId => $args{'Queue'},
432 Load a custom field. If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
438 my $id = shift || '';
440 if ( $id =~ /^\d+$/ ) {
441 return $self->SUPER::Load( $id );
443 return $self->LoadByName( Name => $id );
449 =head2 LoadByName (Queue => QUEUEID, Name => NAME)
451 Loads the Custom field named NAME.
453 Will load a Disabled Custom Field even if there is a non-disabled Custom Field
456 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
458 If the Queue parameter is '0', look for global ticket custom fields.
460 If no queue parameter is specified, look for any and all custom fields with this name.
462 BUG/TODO, this won't let you specify that you only want user or group CFs.
466 # Compatibility for API change after 3.0 beta 1
467 *LoadNameAndQueue = \&LoadByName;
468 # Change after 3.4 beta.
469 *LoadByNameAndQueue = \&LoadByName;
479 unless ( defined $args{'Name'} && length $args{'Name'} ) {
480 $RT::Logger->error("Couldn't load Custom Field without Name");
481 return wantarray ? (0, $self->loc("No name provided")) : 0;
484 # if we're looking for a queue by name, make it a number
485 if ( defined $args{'Queue'} && ($args{'Queue'} =~ /\D/ || !$self->ContextObject) ) {
486 my $QueueObj = RT::Queue->new( $self->CurrentUser );
487 $QueueObj->Load( $args{'Queue'} );
488 $args{'Queue'} = $QueueObj->Id;
489 $self->SetContextObject( $QueueObj )
490 unless $self->ContextObject;
493 # XXX - really naive implementation. Slow. - not really. still just one query
495 my $CFs = RT::CustomFields->new( $self->CurrentUser );
496 $CFs->SetContextObject( $self->ContextObject );
497 my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
498 $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
499 # Don't limit to queue if queue is 0. Trying to do so breaks
500 # RT::Group type CFs.
501 if ( defined $args{'Queue'} ) {
502 $CFs->LimitToQueue( $args{'Queue'} );
505 # When loading by name, we _can_ load disabled fields, but prefer
506 # non-disabled fields.
509 { FIELD => "Disabled", ORDER => 'ASC' },
512 # We only want one entry.
513 $CFs->RowsPerPage(1);
515 # version before 3.8 just returns 0, so we need to test if wantarray to be
516 # backward compatible.
517 return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
519 return $self->LoadById( $first->id );
525 =head2 Custom field values
529 Return a object (collection) of all acceptable values for this Custom Field.
530 Class of the object can vary and depends on the return value
531 of the C<ValuesClass> method.
535 *ValuesObj = \&Values;
540 my $class = $self->ValuesClass;
541 if ( $class ne 'RT::CustomFieldValues') {
542 eval "require $class" or die "$@";
544 my $cf_values = $class->new( $self->CurrentUser );
545 # if the user has no rights, return an empty object
546 if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
547 $cf_values->LimitToCustomField( $self->Id );
549 $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
557 Create a new value for this CustomField. Takes a paramhash containing the elements Name, Description and SortOrder
565 unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) {
566 return (0, $self->loc('Permission Denied'));
570 if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
571 return (0, $self->loc("Can't add a custom field value without a name"));
574 my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
575 return $newval->Create( %args, CustomField => $self->Id );
581 =head3 DeleteValue ID
583 Deletes a value from this custom field by id.
585 Does not remove this value for any article which has had it selected
592 unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) {
593 return (0, $self->loc('Permission Denied'));
596 my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
597 $val_to_del->Load( $id );
598 unless ( $val_to_del->Id ) {
599 return (0, $self->loc("Couldn't find that value"));
601 unless ( $val_to_del->CustomField == $self->Id ) {
602 return (0, $self->loc("That is not a value for this custom field"));
605 my $retval = $val_to_del->Delete;
607 return (0, $self->loc("Custom field value could not be deleted"));
609 return ($retval, $self->loc("Custom field value deleted"));
613 =head2 ValidateQueue Queue
615 Make sure that the name specified is valid
623 return 0 unless length $value;
625 return $self->SUPER::ValidateName($value);
628 =head2 ValidateQueue Queue
630 Make sure that the queue specified is a valid queue name
638 return undef unless defined $id;
639 # 0 means "Global" null would _not_ be ok.
640 return 1 if $id eq '0';
642 my $q = RT::Queue->new( RT->SystemUser );
644 return undef unless $q->id;
652 Retuns an array of the types of CustomField that are supported
657 return (sort {(($FieldTypes{$a}{sort_order}||999) <=> ($FieldTypes{$b}{sort_order}||999)) or ($a cmp $b)} keys %FieldTypes);
661 =head2 IsSelectionType
663 Retuns a boolean value indicating whether the C<Values> method makes sense
664 to this Custom Field.
668 sub IsSelectionType {
670 my $type = @_? shift : $self->Type;
671 return undef unless $type;
672 return $FieldTypes{$type}->{selection_type};
677 =head2 IsExternalValues
681 sub IsExternalValues {
683 return 0 unless $self->IsSelectionType( @_ );
684 return $self->ValuesClass eq 'RT::CustomFieldValues'? 0 : 1;
689 return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues';
694 my $class = shift || 'RT::CustomFieldValues';
696 if ( $class eq 'RT::CustomFieldValues' ) {
697 return $self->_Set( Field => 'ValuesClass', Value => undef, @_ );
700 return (0, $self->loc("This Custom Field can not have list of values"))
701 unless $self->IsSelectionType;
703 unless ( $self->ValidateValuesClass( $class ) ) {
704 return (0, $self->loc("Invalid Custom Field values source"));
706 return $self->_Set( Field => 'ValuesClass', Value => $class, @_ );
709 sub ValidateValuesClass {
713 return 1 if !$class || $class eq 'RT::CustomFieldValues';
714 return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesSources');
719 =head2 FriendlyType [TYPE, MAX_VALUES]
721 Returns a localized human-readable version of the custom field type.
722 If a custom field type is specified as the parameter, the friendly type for that type will be returned
729 my $type = @_ ? shift : $self->Type;
730 my $max = @_ ? shift : $self->MaxValues;
731 $max = 0 unless $max;
733 if (my $friendly_type = $FieldTypes{$type}->{labels}->[$max>2 ? 2 : $max]) {
734 return ( $self->loc( $friendly_type, $max ) );
737 return ( $self->loc( $type ) );
741 sub FriendlyTypeComposite {
743 my $composite = shift || $self->TypeComposite;
744 return $self->FriendlyType(split(/-/, $composite, 2));
748 =head2 ValidateType TYPE
750 Takes a single string. returns true if that string is a value
760 if ( $type =~ s/(?:Single|Multiple)$// ) {
761 $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
764 if ( $FieldTypes{$type} ) {
776 if ($type =~ s/(?:(Single)|Multiple)$//) {
777 $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
778 $self->SetMaxValues($1 ? 1 : 0);
780 $self->_Set(Field => 'Type', Value =>$type);
783 =head2 SetPattern STRING
785 Takes a single string representing a regular expression. Performs basic
786 validation on that regex, and sets the C<Pattern> field for the CF if it
795 my ($ok, $msg) = $self->_IsValidRegex($regex);
797 return $self->_Set(Field => 'Pattern', Value => $regex);
800 return (0, $self->loc("Invalid pattern: [_1]", $msg));
804 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
806 Tests if the string contains an invalid regex.
812 my $regex = shift or return (1, 'valid');
815 local $SIG{__DIE__} = sub { 1 };
816 local $SIG{__WARN__} = sub { 1 };
818 if (eval { qr/$regex/; 1 }) {
823 $err =~ s{[,;].*}{}; # strip debug info from error
831 Returns true if this CustomField only accepts a single value.
832 Returns false if it accepts multiple values
838 if (($self->MaxValues||0) == 1) {
846 sub UnlimitedValues {
848 if (($self->MaxValues||0) == 0) {
857 =head2 CurrentUserHasRight RIGHT
859 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
863 sub CurrentUserHasRight {
867 return $self->CurrentUser->HasRight(
873 =head2 ACLEquivalenceObjects
875 Returns list of objects via which users can get rights on this custom field. For custom fields
876 these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
880 sub ACLEquivalenceObjects {
883 my $ctx = $self->ContextObject
885 return ($ctx, $ctx->ACLEquivalenceObjects);
888 =head2 ContextObject and SetContextObject
890 Set or get a context for this object. It can be ticket, queue or another object
891 this CF applies to. Used for ACL control, for example SeeCustomField can be granted on
892 queue level to allow people to see all fields applied to the queue.
896 sub SetContextObject {
898 return $self->{'context_object'} = shift;
903 return $self->{'context_object'};
906 sub ValidContextType {
911 $valid{$_}++ for split '-', $self->LookupType;
912 delete $valid{'RT::Transaction'};
914 return $valid{$class};
917 =head2 LoadContextObject
919 Takes an Id for a Context Object and loads the right kind of RT::Object
920 for this particular Custom Field (based on the LookupType) and returns it.
921 This is a good way to ensure you don't try to use a Queue as a Context
922 Object on a User Custom Field.
926 sub LoadContextObject {
929 my $contextid = shift;
931 unless ( $self->ValidContextType($type) ) {
932 RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id);
936 my $context_object = $type->new( $self->CurrentUser );
937 my ($id, $msg) = $context_object->LoadById( $contextid );
939 RT->Logger->debug("Invalid ContextObject id: $msg");
942 return $context_object;
945 =head2 ValidateContextObject
947 Ensure that a given ContextObject applies to this Custom Field.
948 For custom fields that are assigned to Queues or to Classes, this checks that the Custom
949 Field is actually applied to that objects. For Global Custom Fields, it returns true
950 as long as the Object is of the right type, because you may be using
951 your permissions on a given Queue of Class to see a Global CF.
952 For CFs that are only applied Globally, you don't need a ContextObject.
956 sub ValidateContextObject {
960 return 1 if $self->IsApplied(0);
962 # global only custom fields don't have objects
963 # that should be used as context objects.
964 return if $self->ApplyGlobally;
966 # Otherwise, make sure we weren't passed a user object that we're
967 # supposed to treat as a queue.
968 return unless $self->ValidContextType(ref $object);
970 # Check that it is applied correctly
971 my ($applied_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects);
972 return unless $applied_to;
973 return $self->IsApplied($applied_to->id);
980 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
981 return ( 0, $self->loc('Permission Denied') );
983 return $self->SUPER::_Set( @_ );
991 Takes the name of a table column.
992 Returns its value as a string, if the user passes an ACL check
998 return undef unless $self->id;
1000 # we need to do the rights check
1001 unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
1003 "Permission denied. User #". $self->CurrentUser->id
1004 ." has no SeeCustomField right on CF #". $self->id
1008 return $self->__Value( @_ );
1015 1 will cause this custom field to no longer be avaialble for objects.
1016 0 will re-enable this field.
1021 =head2 SetTypeComposite
1023 Set this custom field's type and maximum values as a composite value
1027 sub SetTypeComposite {
1029 my $composite = shift;
1031 my $old = $self->TypeComposite;
1033 my ($type, $max_values) = split(/-/, $composite, 2);
1034 if ( $type ne $self->Type ) {
1035 my ($status, $msg) = $self->SetType( $type );
1036 return ($status, $msg) unless $status;
1038 if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
1039 my ($status, $msg) = $self->SetMaxValues( $max_values );
1040 return ($status, $msg) unless $status;
1042 my $render = $self->RenderType;
1043 if ( $render and not grep { $_ eq $render } $self->RenderTypes ) {
1044 # We switched types and our render type is no longer valid, so unset it
1045 # and use the default
1046 $self->SetRenderType( undef );
1048 return 1, $self->loc(
1049 "Type changed from '[_1]' to '[_2]'",
1050 $self->FriendlyTypeComposite( $old ),
1051 $self->FriendlyTypeComposite( $composite ),
1055 =head2 TypeComposite
1057 Returns a composite value composed of this object's type and maximum values
1064 return join '-', ($self->Type || ''), ($self->MaxValues || 0);
1067 =head2 TypeComposites
1069 Returns an array of all possible composite values for custom fields.
1073 sub TypeComposites {
1075 return grep !/(?:[Tt]ext|Combobox|Date|DateTime|TimeValue)-0/, map { ("$_-1", "$_-0") } $self->Types;
1080 Returns the type of form widget to render for this custom field. Currently
1081 this only affects fields which return true for L</HasRenderTypes>.
1087 return '' unless $self->HasRenderTypes;
1089 return $self->_Value( 'RenderType', @_ )
1090 || $self->DefaultRenderType;
1093 =head2 SetRenderType TYPE
1095 Sets this custom field's render type.
1102 return (0, $self->loc("This custom field has no Render Types"))
1103 unless $self->HasRenderTypes;
1105 if ( !$type || $type eq $self->DefaultRenderType ) {
1106 return $self->_Set( Field => 'RenderType', Value => undef, @_ );
1109 if ( not grep { $_ eq $type } $self->RenderTypes ) {
1110 return (0, $self->loc("Invalid Render Type for custom field of type [_1]",
1111 $self->FriendlyType));
1114 # XXX: Remove this restriction once we support lists and cascaded selects
1115 if ( $self->BasedOnObj->id and $type =~ /List/ ) {
1116 return (0, $self->loc("We can't currently render as a List when basing categories on another custom field. Please use another render type."));
1119 return $self->_Set( Field => 'RenderType', Value => $type, @_ );
1122 =head2 DefaultRenderType [TYPE COMPOSITE]
1124 Returns the default render type for this custom field's type or the TYPE
1125 COMPOSITE specified as an argument.
1129 sub DefaultRenderType {
1131 my $composite = @_ ? shift : $self->TypeComposite;
1132 my ($type, $max) = split /-/, $composite, 2;
1133 return unless $type and $self->HasRenderTypes($composite);
1134 return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0];
1137 =head2 HasRenderTypes [TYPE_COMPOSITE]
1139 Returns a boolean value indicating whether the L</RenderTypes> and
1140 L</RenderType> methods make sense for this custom field.
1142 Currently true only for type C<Select>.
1146 sub HasRenderTypes {
1148 my ($type, $max) = split /-/, (@_ ? shift : $self->TypeComposite), 2;
1149 return undef unless $type;
1150 return defined $FieldTypes{$type}->{render_types}
1151 ->{ $max == 1 ? 'single' : 'multiple' };
1154 =head2 RenderTypes [TYPE COMPOSITE]
1156 Returns the valid render types for this custom field's type or the TYPE
1157 COMPOSITE specified as an argument.
1163 my $composite = @_ ? shift : $self->TypeComposite;
1164 my ($type, $max) = split /-/, $composite, 2;
1165 return unless $type and $self->HasRenderTypes($composite);
1166 return @{$FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }};
1169 =head2 SetLookupType
1171 Autrijus: care to doc how LookupTypes work?
1178 if ( $lookup ne $self->LookupType ) {
1179 # Okay... We need to invalidate our existing relationships
1180 my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
1181 $ObjectCustomFields->LimitToCustomField($self->Id);
1182 $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
1184 return $self->_Set(Field => 'LookupType', Value =>$lookup);
1189 Returns an array of LookupTypes available
1196 return keys %FRIENDLY_OBJECT_TYPES;
1199 my @FriendlyObjectTypes = (
1200 "[_1] objects", # loc
1201 "[_1]'s [_2] objects", # loc
1202 "[_1]'s [_2]'s [_3] objects", # loc
1205 =head2 FriendlyLookupType
1207 Returns a localized description of the type of this custom field
1211 sub FriendlyLookupType {
1213 my $lookup = shift || $self->LookupType;
1215 return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
1216 if (defined $FRIENDLY_OBJECT_TYPES{$lookup} );
1218 my @types = map { s/^RT::// ? $self->loc($_) : $_ }
1219 grep { defined and length }
1220 split( /-/, $lookup )
1222 return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
1225 sub RecordClassFromLookupType {
1227 my ($class) = ($self->LookupType =~ /^([^-]+)/);
1230 "Custom Field #". $self->id
1231 ." has incorrect LookupType '". $self->LookupType ."'"
1238 sub CollectionClassFromLookupType {
1241 my $record_class = $self->RecordClassFromLookupType;
1242 return undef unless $record_class;
1244 my $collection_class;
1245 if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
1246 $collection_class = $record_class.'Collection';
1247 } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
1248 $collection_class = $record_class.'es';
1249 } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
1250 $collection_class = $record_class.'s';
1252 $RT::Logger->error("Can not find a collection class for record class '$record_class'");
1255 return $collection_class;
1258 =head1 ApplyGlobally
1260 Certain custom fields (users, groups) should only be applied globally
1261 but rather than regexing in code for LookupType =~ RT::Queue, we'll codify
1269 return ($self->LookupType =~ /^RT::(?:Group|User)/io);
1275 Returns collection with objects this custom field is applied to.
1276 Class of the collection depends on L</LookupType>.
1277 See all L</NotAppliedTo> .
1279 Doesn't takes into account if object is applied globally.
1286 my ($res, $ocfs_alias) = $self->_AppliedTo;
1287 return $res unless $res;
1290 ALIAS => $ocfs_alias,
1292 OPERATOR => 'IS NOT',
1301 Returns collection with objects this custom field is not applied to.
1302 Class of the collection depends on L</LookupType>.
1303 See all L</AppliedTo> .
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,
1328 my ($class) = $self->CollectionClassFromLookupType;
1329 return undef unless $class;
1331 my $res = $class->new( $self->CurrentUser );
1333 # If CF is a Group CF, only display user-defined groups
1334 if ( $class eq 'RT::Groups' ) {
1335 $res->LimitToUserDefinedGroups;
1338 $res->OrderBy( FIELD => 'Name' );
1339 my $ocfs_alias = $res->Join(
1343 TABLE2 => 'ObjectCustomFields',
1344 FIELD2 => 'ObjectId',
1347 LEFTJOIN => $ocfs_alias,
1348 ALIAS => $ocfs_alias,
1349 FIELD => 'CustomField',
1352 return ($res, $ocfs_alias);
1357 Takes object id and returns corresponding L<RT::ObjectCustomField>
1358 record if this custom field is applied to the object. Use 0 to check
1359 if custom field is applied globally.
1366 my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1367 $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1368 return undef unless $ocf->id;
1372 =head2 AddToObject OBJECT
1374 Add this custom field as a custom field for a single object, such as a queue or group.
1384 my $id = $object->Id || 0;
1386 unless (index($self->LookupType, ref($object)) == 0) {
1387 return ( 0, $self->loc('Lookup type mismatch') );
1390 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1391 return ( 0, $self->loc('Permission Denied') );
1394 if ( $self->IsApplied( $id ) ) {
1395 return ( 0, $self->loc("Custom field is already applied to the object") );
1400 return (0, $self->loc("Couldn't apply custom field to an object as it's global already") )
1401 if $self->IsApplied( 0 );
1404 my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
1405 $applied->LimitToCustomField( $self->id );
1406 while ( my $record = $applied->Next ) {
1411 my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1412 my ( $oid, $msg ) = $ocf->Create(
1413 ObjectId => $id, CustomField => $self->id,
1415 return ( $oid, $msg );
1419 =head2 RemoveFromObject OBJECT
1421 Remove this custom field for a single object, such as a queue or group.
1427 sub RemoveFromObject {
1430 my $id = $object->Id || 0;
1432 unless (index($self->LookupType, ref($object)) == 0) {
1433 return ( 0, $self->loc('Object type mismatch') );
1436 unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1437 return ( 0, $self->loc('Permission Denied') );
1440 my $ocf = $self->IsApplied( $id );
1442 return ( 0, $self->loc("This custom field does not apply to that object") );
1445 # XXX: Delete doesn't return anything
1446 my ( $oid, $msg ) = $ocf->Delete;
1447 return ( $oid, $msg );
1451 =head2 AddValueForObject HASH
1453 Adds a custom field value for a record object of some kind.
1454 Takes a param hash of
1468 sub AddValueForObject {
1473 LargeContent => undef,
1474 ContentType => undef,
1477 my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1479 unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1480 return ( 0, $self->loc('Permission Denied') );
1483 unless ( $self->MatchPattern($args{'Content'}) ) {
1484 return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1487 $RT::Handle->BeginTransaction;
1489 if ( $self->MaxValues ) {
1490 my $current_values = $self->ValuesForObject($obj);
1491 my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1493 # (The +1 is for the new value we're adding)
1495 # If we have a set of current values and we've gone over the maximum
1496 # allowed number of values, we'll need to delete some to make room.
1497 # which former values are blown away is not guaranteed
1499 while ($extra_values) {
1500 my $extra_item = $current_values->Next;
1501 unless ( $extra_item->id ) {
1502 $RT::Logger->crit( "We were just asked to delete "
1503 ."a custom field value that doesn't exist!" );
1504 $RT::Handle->Rollback();
1507 $extra_item->Delete;
1512 if (my $canonicalizer = $self->can('_CanonicalizeValue'.$self->Type)) {
1513 $canonicalizer->($self, \%args);
1518 my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1519 my ($val, $msg) = $newval->Create(
1520 ObjectType => ref($obj),
1521 ObjectId => $obj->Id,
1522 Content => $args{'Content'},
1523 LargeContent => $args{'LargeContent'},
1524 ContentType => $args{'ContentType'},
1525 CustomField => $self->Id
1529 $RT::Handle->Rollback();
1530 return ($val, $self->loc("Couldn't create record: [_1]", $msg));
1533 $RT::Handle->Commit();
1540 sub _CanonicalizeValueDateTime {
1543 my $DateObj = RT::Date->new( $self->CurrentUser );
1544 $DateObj->Set( Format => 'unknown',
1545 Value => $args->{'Content'} );
1546 $args->{'Content'} = $DateObj->ISO;
1549 # For date, we need to store Content as ISO date
1550 sub _CanonicalizeValueDate {
1554 # in case user input date with time, let's omit it by setting timezone
1555 # to utc so "hour" won't affect "day"
1556 my $DateObj = RT::Date->new( $self->CurrentUser );
1557 $DateObj->Set( Format => 'unknown',
1558 Value => $args->{'Content'},
1560 $args->{'Content'} = $DateObj->Date( Timezone => 'user' );
1563 =head2 MatchPattern STRING
1565 Tests the incoming string against the Pattern of this custom field object
1566 and returns a boolean; returns true if the Pattern is empty.
1572 my $regex = $self->Pattern or return 1;
1574 return (( defined $_[0] ? $_[0] : '') =~ $regex);
1580 =head2 FriendlyPattern
1582 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1587 sub FriendlyPattern {
1589 my $regex = $self->Pattern;
1591 return '' unless length $regex;
1592 if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1593 return '[' . $self->loc($1) . ']';
1603 =head2 DeleteValueForObject HASH
1605 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1607 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1611 sub DeleteValueForObject {
1613 my %args = ( Object => undef,
1619 unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1620 return (0, $self->loc('Permission Denied'));
1623 my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1625 if (my $id = $args{'Id'}) {
1628 unless ($oldval->id) {
1629 $oldval->LoadByObjectContentAndCustomField(
1630 Object => $args{'Object'},
1631 Content => $args{'Content'},
1632 CustomField => $self->Id,
1637 # check to make sure we found it
1638 unless ($oldval->Id) {
1639 return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1642 # for single-value fields, we need to validate that empty string is a valid value for it
1643 if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1644 return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1649 my $ret = $oldval->Delete();
1651 return(0, $self->loc("Custom field value could not be found"));
1653 return($oldval->Id, $self->loc("Custom field value deleted"));
1657 =head2 ValuesForObject OBJECT
1659 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT
1663 sub ValuesForObject {
1667 my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
1668 unless ($self->id and $self->CurrentUserHasRight('SeeCustomField')) {
1669 # Return an empty object if they have no rights to see
1670 $values->Limit( FIELD => "id", VALUE => 0, SUBCLAUSE => "ACL" );
1674 $values->LimitToCustomField($self->Id);
1675 $values->LimitToObject($object);
1681 =head2 _ForObjectType PATH FRIENDLYNAME
1683 Tell RT that a certain object accepts custom fields
1687 'RT::Queue-RT::Ticket' => "Tickets", # loc
1688 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", # loc
1689 'RT::User' => "Users", # loc
1690 'RT::Group' => "Groups", # loc
1691 'RT::Queue' => "Queues", # loc
1693 This is a class method.
1697 sub _ForObjectType {
1700 my $friendly_name = shift;
1702 $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1707 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1709 Gets or sets the C<IncludeContentForValue> for this custom field. RT
1710 uses this field to automatically include content into the user's browser
1711 as they display records with custom fields in RT.
1715 sub SetIncludeContentForValue {
1716 shift->IncludeContentForValue(@_);
1718 sub IncludeContentForValue{
1720 $self->_URLTemplate('IncludeContentForValue', @_);
1725 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1727 Gets or sets the C<LinkValueTo> for this custom field. RT
1728 uses this field to make custom field values into hyperlinks in the user's
1729 browser as they display records with custom fields in RT.
1734 sub SetLinkValueTo {
1735 shift->LinkValueTo(@_);
1740 $self->_URLTemplate('LinkValueTo', @_);
1745 =head2 _URLTemplate NAME [VALUE]
1747 With one argument, returns the _URLTemplate named C<NAME>, but only if
1748 the current user has the right to see this custom field.
1750 With two arguments, attemptes to set the relevant template value.
1756 my $template_name = shift;
1760 unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1761 return ( 0, $self->loc('Permission Denied') );
1763 $self->SetAttribute( Name => $template_name, Content => $value );
1764 return ( 1, $self->loc('Updated') );
1766 unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1770 my @attr = $self->Attributes->Named($template_name);
1771 my $attr = shift @attr;
1773 if ($attr) { return $attr->Content }
1782 return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1783 unless defined $value and length $value;
1785 my $cf = RT::CustomField->new( $self->CurrentUser );
1786 $cf->SetContextObject( $self->ContextObject );
1787 $cf->Load( ref $value ? $value->id : $value );
1789 return (0, "Permission denied")
1790 unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
1792 # XXX: Remove this restriction once we support lists and cascaded selects
1793 if ( $self->RenderType =~ /List/ ) {
1794 return (0, $self->loc("We can't currently render as a List when basing categories on another custom field. Please use another render type."));
1797 return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1803 my $obj = RT::CustomField->new( $self->CurrentUser );
1804 $obj->SetContextObject( $self->ContextObject );
1805 if ( $self->BasedOn ) {
1806 $obj->Load( $self->BasedOn );
1813 my $tag = $self->FirstAttribute( 'UILocation' );
1814 return $tag ? $tag->Content : '';
1821 return $self->SetAttribute( Name => 'UILocation', Content => $tag );
1824 return $self->DeleteAttribute('UILocation');
1830 $self->FirstAttribute('NoClone') ? 1 : '';
1837 return $self->SetAttribute( Name => 'NoClone', Content => 1 );
1839 return $self->DeleteAttribute('NoClone');
1846 Returns the current value of id.
1847 (In the database, id is stored as int(11).)
1855 Returns the current value of Name.
1856 (In the database, Name is stored as varchar(200).)
1860 =head2 SetName VALUE
1864 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1865 (In the database, Name will be stored as a varchar(200).)
1873 Returns the current value of Type.
1874 (In the database, Type is stored as varchar(200).)
1878 =head2 SetType VALUE
1882 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1883 (In the database, Type will be stored as a varchar(200).)
1891 Returns the current value of RenderType.
1892 (In the database, RenderType is stored as varchar(64).)
1896 =head2 SetRenderType VALUE
1899 Set RenderType to VALUE.
1900 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1901 (In the database, RenderType will be stored as a varchar(64).)
1909 Returns the current value of MaxValues.
1910 (In the database, MaxValues is stored as int(11).)
1914 =head2 SetMaxValues VALUE
1917 Set MaxValues to VALUE.
1918 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1919 (In the database, MaxValues will be stored as a int(11).)
1927 Returns the current value of Pattern.
1928 (In the database, Pattern is stored as text.)
1932 =head2 SetPattern VALUE
1935 Set Pattern to VALUE.
1936 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1937 (In the database, Pattern will be stored as a text.)
1945 Returns the current value of Repeated.
1946 (In the database, Repeated is stored as smallint(6).)
1950 =head2 SetRepeated VALUE
1953 Set Repeated to VALUE.
1954 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1955 (In the database, Repeated will be stored as a smallint(6).)
1963 Returns the current value of BasedOn.
1964 (In the database, BasedOn is stored as int(11).)
1968 =head2 SetBasedOn VALUE
1971 Set BasedOn to VALUE.
1972 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1973 (In the database, BasedOn will be stored as a int(11).)
1981 Returns the current value of Description.
1982 (In the database, Description is stored as varchar(255).)
1986 =head2 SetDescription VALUE
1989 Set Description to VALUE.
1990 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1991 (In the database, Description will be stored as a varchar(255).)
1999 Returns the current value of SortOrder.
2000 (In the database, SortOrder is stored as int(11).)
2004 =head2 SetSortOrder VALUE
2007 Set SortOrder to VALUE.
2008 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2009 (In the database, SortOrder will be stored as a int(11).)
2017 Returns the current value of LookupType.
2018 (In the database, LookupType is stored as varchar(255).)
2022 =head2 SetLookupType VALUE
2025 Set LookupType to VALUE.
2026 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2027 (In the database, LookupType will be stored as a varchar(255).)
2035 Returns the current value of Creator.
2036 (In the database, Creator is stored as int(11).)
2044 Returns the current value of Created.
2045 (In the database, Created is stored as datetime.)
2051 =head2 LastUpdatedBy
2053 Returns the current value of LastUpdatedBy.
2054 (In the database, LastUpdatedBy is stored as int(11).)
2062 Returns the current value of LastUpdated.
2063 (In the database, LastUpdated is stored as datetime.)
2071 Returns the current value of Disabled.
2072 (In the database, Disabled is stored as smallint(6).)
2076 =head2 SetDisabled VALUE
2079 Set Disabled to VALUE.
2080 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2081 (In the database, Disabled will be stored as a smallint(6).)
2088 sub _CoreAccessible {
2092 {read => 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 => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
2096 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
2098 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
2100 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2102 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''},
2104 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
2106 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
2108 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
2110 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
2112 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2114 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
2116 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2118 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
2120 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
2122 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
2124 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
2130 RT::Base->_ImportOverlays();