TimeWorked-like custom fields, RT#11168
[freeside.git] / rt / lib / RT / CustomField_Overlay.pm
index 743c85c..5e868d1 100644 (file)
@@ -1,38 +1,40 @@
 # BEGIN BPS TAGGED BLOCK {{{
-# 
+#
 # COPYRIGHT:
-#  
-# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC 
-#                                          <jesse@bestpractical.com>
-# 
+#
+# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+#                                          <sales@bestpractical.com>
+#
 # (Except where explicitly superseded by other copyright notices)
-# 
-# 
+#
+#
 # LICENSE:
-# 
+#
 # This work is made available to you under the terms of Version 2 of
 # the GNU General Public License. A copy of that license should have
 # been provided with this software, but in any event can be snarfed
 # from www.gnu.org.
-# 
+#
 # This work is distributed in the hope that it will be useful, but
 # WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 # General Public License for more details.
-# 
+#
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
-# 
-# 
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
 # CONTRIBUTION SUBMISSION POLICY:
-# 
+#
 # (The following paragraph is not intended to limit the rights granted
 # to you to modify and distribute this software under the terms of
 # the GNU General Public License and is only of importance to you if
 # you choose to contribute your changes and enhancements to the
 # community by submitting them to Best Practical Solutions, LLC.)
-# 
+#
 # By intentionally submitting any modifications, corrections or
 # derivatives to this work, or any other work intended for use with
 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
 # royalty-free, perpetual, license to use, copy, create derivative
 # works based on those contributions, and sublicense and distribute
 # those contributions and any derivatives thereof.
-# 
+#
 # END BPS TAGGED BLOCK }}}
+
 package RT::CustomField;
 
 use strict;
 no warnings qw(redefine);
 
-use vars qw(%FieldTypes $RIGHTS %FRIENDLY_OBJECT_TYPES);
-
 use RT::CustomFieldValues;
+use RT::ObjectCustomFields;
 use RT::ObjectCustomFieldValues;
 
 
-%FieldTypes = (
+our %FieldTypes = (
     Select => [
-        'Select multiple values',      # loc
-        'Select one value',            # loc
-        'Select up to [_1] values',    # loc
+        'Select multiple values',    # loc
+        'Select one value',        # loc
+        'Select up to [_1] values',    # loc
     ],
     Freeform => [
-        'Enter multiple values',       # loc
-        'Enter one value',             # loc
-        'Enter up to [_1] values',     # loc
+        'Enter multiple values',    # loc
+        'Enter one value',        # loc
+        'Enter up to [_1] values',    # loc
     ],
     Text => [
-        'Fill in multiple text areas', # loc
-        'Fill in one text area',       # loc
+        'Fill in multiple text areas',    # loc
+        'Fill in one text area',    # loc
         'Fill in up to [_1] text areas',# loc
     ],
     Wikitext => [
-        'Fill in multiple wikitext areas',     # loc
-        'Fill in one wikitext area',   # loc
+        'Fill in multiple wikitext areas',    # loc
+        'Fill in one wikitext area',    # loc
         'Fill in up to [_1] wikitext areas',# loc
     ],
     Image => [
-        'Upload multiple images',      # loc
-        'Upload one image',            # loc
-        'Upload up to [_1] images',    # loc
+        'Upload multiple images',    # loc
+        'Upload one image',        # loc
+        'Upload up to [_1] images',    # loc
     ],
     Binary => [
-        'Upload multiple files',       # loc
-        'Upload one file',             # loc
-        'Upload up to [_1] files',     # loc
+        'Upload multiple files',    # loc
+        'Upload one file',        # loc
+        'Upload up to [_1] files',    # loc
+    ],
+    Combobox => [
+        'Combobox: Select or enter multiple values',    # loc
+        'Combobox: Select or enter one value',        # loc
+        'Combobox: Select or enter up to [_1] values',    # loc
+    ],
+    Autocomplete => [
+        'Enter multiple values with autocompletion',    # loc
+        'Enter one value with autocompletion',            # loc
+        'Enter up to [_1] values with autocompletion',    # loc
+    ],
+    Date => [
+        'Select multiple dates',       # loc
+        'Select date',                 # loc
+        'Select up to [_1] dates',     # loc
+    ],
+    TimeValue => [
+        'Enter multiple time values (UNSUPPORTED)',
+        'Enter a time value',
+        'Enter [_1] time values (UNSUPPORTED)',
     ],
 );
 
 
-%FRIENDLY_OBJECT_TYPES =  ();
+our %FRIENDLY_OBJECT_TYPES =  ();
 
 RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket' => "Tickets", );    #loc
 RT::CustomField->_ForObjectType(
     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", );    #loc
 RT::CustomField->_ForObjectType( 'RT::User'  => "Users", );                           #loc
+RT::CustomField->_ForObjectType( 'RT::Queue'  => "Queues", );                         #loc
 RT::CustomField->_ForObjectType( 'RT::Group' => "Groups", );                          #loc
 
-$RIGHTS = {
+our $RIGHTS = {
     SeeCustomField            => 'See custom fields',       # loc_pair
     AdminCustomField          => 'Create, delete and modify custom fields',        # loc_pair
+    AdminCustomFieldValues    => 'Create, delete and modify custom fields values',        # loc_pair
     ModifyCustomField         => 'Add, delete and modify custom field values for objects' #loc_pair
-
 };
 
 # Tell RT::ACE that this sort of object can get acls granted
@@ -110,23 +133,34 @@ foreach my $right ( keys %{$RIGHTS} ) {
     $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
 }
 
+=head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...]
+
+Adds the given rights to the list of possible rights.  This method
+should be called during server startup, not at runtime.
+
+=cut
+
+sub AddRights {
+    my $self = shift;
+    my %new = @_;
+    $RIGHTS = { %$RIGHTS, %new };
+    %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES,
+                                      map { lc($_) => $_ } keys %new);
+}
+
 sub AvailableRights {
     my $self = shift;
-    return($RIGHTS);
+    return $RIGHTS;
 }
 
 =head1 NAME
 
-  RT::CustomField_Overlay 
+  RT::CustomField_Overlay - overlay for RT::CustomField
 
 =head1 DESCRIPTION
 
 =head1 'CORE' METHODS
 
-=cut
-
-
-
 =head2 Create PARAMHASH
 
 Create takes a hash of values and creates a row in the database:
@@ -141,49 +175,48 @@ Create takes a hash of values and creates a row in the database:
   varchar(255) 'LookupType'.
   smallint(6) 'Disabled'.
 
-  'LookupType' is generally the result of either 
-  RT::Ticket->CustomFieldLookupType or RT::Transaction->CustomFieldLookupType
+C<LookupType> is generally the result of either
+C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType>.
 
 =cut
 
-
-
-
 sub Create {
     my $self = shift;
-    my %args = ( 
-                Name => '',
-                Type => '',
-               MaxValues => '0',
-               Pattern  => '',
-                Description => '',
-                Disabled => '0',
-               LookupType  => '',
-               Repeated  => '0',
-
-                 @_);
-
-    unless ($self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField')) {
+    my %args = (
+        Name        => '',
+        Type        => '',
+        MaxValues   => 0,
+        Pattern     => '',
+        Description => '',
+        Disabled    => 0,
+        LookupType  => '',
+        Repeated    => 0,
+        LinkValueTo => '',
+        IncludeContentForValue => '',
+        @_,
+    );
+
+    unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) {
         return (0, $self->loc('Permission Denied'));
     }
 
-
-    if ($args{TypeComposite}) {
-       @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
+    if ( $args{TypeComposite} ) {
+        @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
     }
-    elsif ($args{Type} =~ s/(?:(Single)|Multiple)$//) {
-       # old style Type string
-       $args{'MaxValues'} = $1 ? 1 : 0;
+    elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) {
+        # old style Type string
+        $args{'MaxValues'} = $1 ? 1 : 0;
     }
-    
+    $args{'MaxValues'} = int $args{'MaxValues'};
+
     if ( !exists $args{'Queue'}) {
-       # do nothing -- things below are strictly backward compat
+    # do nothing -- things below are strictly backward compat
     }
     elsif (  ! $args{'Queue'} ) {
         unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
             return ( 0, $self->loc('Permission Denied') );
         }
-       $args{'LookupType'} = 'RT::Queue-RT::Ticket';
+        $args{'LookupType'} = 'RT::Queue-RT::Ticket';
     }
     else {
         my $queue = RT::Queue->new($self->CurrentUser);
@@ -194,29 +227,59 @@ sub Create {
         unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
             return ( 0, $self->loc('Permission Denied') );
         }
-       $args{'LookupType'} = 'RT::Queue-RT::Ticket';
-    }
-    my $rv = $self->SUPER::Create(
-                         Name => $args{'Name'},
-                         Type => $args{'Type'},
-                         MaxValues => $args{'MaxValues'},
-                         Pattern  => $args{'Pattern'},
-                         Description => $args{'Description'},
-                         Disabled => $args{'Disabled'},
-                        LookupType => $args{'LookupType'},
-                        Repeated => $args{'Repeated'},
-);
+        $args{'LookupType'} = 'RT::Queue-RT::Ticket';
+        $args{'Queue'} = $queue->Id;
+    }
+
+    my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} );
+    return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok;
+
+    if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) {
+        $RT::Logger->warning("Support for 'multiple' Texts or Comboboxes is not implemented");
+        $args{'MaxValues'} = 1;
+    }
+
+    (my $rv, $msg) = $self->SUPER::Create(
+        Name        => $args{'Name'},
+        Type        => $args{'Type'},
+        MaxValues   => $args{'MaxValues'},
+        Pattern     => $args{'Pattern'},
+        Description => $args{'Description'},
+        Disabled    => $args{'Disabled'},
+        LookupType  => $args{'LookupType'},
+        Repeated    => $args{'Repeated'},
+    );
+
+    if ( exists $args{'LinkValueTo'}) {
+       $self->SetLinkValueTo($args{'LinkValueTo'});
+    }
+
+    if ( exists $args{'IncludeContentForValue'}) {
+       $self->SetIncludeContentForValue($args{'IncludeContentForValue'});
+    }
+
+    if ( exists $args{'ValuesClass'} ) {
+        $self->SetValuesClass( $args{'ValuesClass'} );
+    }
+
+    if ( exists $args{'BasedOn'} ) {
+        $self->SetBasedOn( $args{'BasedOn'} );
+    }
+
+    if ( exists $args{'UILocation'} ) {
+        $self->SetUILocation( $args{'UILocation'} );
+    }
 
-    return $rv unless exists $args{'Queue'};
+    return ($rv, $msg) unless exists $args{'Queue'};
 
     # Compat code -- create a new ObjectCustomField mapping
-    my $OCF = RT::ObjectCustomField->new($self->CurrentUser);
+    my $OCF = RT::ObjectCustomField->new( $self->CurrentUser );
     $OCF->Create(
-       CustomField => $self->Id,
-       ObjectId => $args{'Queue'},
+        CustomField => $self->Id,
+        ObjectId => $args{'Queue'},
     );
 
-    return $rv;
+    return ($rv, $msg);
 }
 
 =head2 Load ID/NAME
@@ -225,25 +288,27 @@ Load a custom field.  If the value handed in is an integer, load by custom field
 
 =cut
 
-
 sub Load {
     my $self = shift;
-    my $id = shift;
+    my $id = shift || '';
 
-    if ($id =~ /^\d+$/) {
-        return ($self->SUPER::Load($id));
+    if ( $id =~ /^\d+$/ ) {
+        return $self->SUPER::Load( $id );
     } else {
-        return($self->LoadByName(Name => $id));
+        return $self->LoadByName( Name => $id );
     }
 }
 
 
 # {{{ sub LoadByName
 
-=head2  LoadByName (Queue => QUEUEID, Name => NAME)
+=head2 LoadByName (Queue => QUEUEID, Name => NAME)
 
 Loads the Custom field named NAME.
 
+Will load a Disabled Custom Field even if there is a non-disabled Custom Field
+with the same Name.
+
 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
 
 If the Queue parameter is '0', look for global ticket custom fields.
@@ -267,115 +332,100 @@ sub LoadByName {
         @_,
     );
 
+    unless ( defined $args{'Name'} && length $args{'Name'} ) {
+        $RT::Logger->error("Couldn't load Custom Field without Name");
+        return wantarray ? (0, $self->loc("No name provided")) : 0;
+    }
+
     # if we're looking for a queue by name, make it a number
-    if  (defined $args{'Queue'}  &&  $args{'Queue'} !~ /^\d+$/) {
-       my $QueueObj = RT::Queue->new($self->CurrentUser);
-       $QueueObj->Load($args{'Queue'});
-       $args{'Queue'} = $QueueObj->Id;
+    if ( defined $args{'Queue'} && $args{'Queue'} =~ /\D/ ) {
+        my $QueueObj = RT::Queue->new( $self->CurrentUser );
+        $QueueObj->Load( $args{'Queue'} );
+        $args{'Queue'} = $QueueObj->Id;
     }
 
     # XXX - really naive implementation.  Slow. - not really. still just one query
 
-    my $CFs = RT::CustomFields->new($self->CurrentUser);
-
-    $CFs->Limit( FIELD => 'Name', VALUE => $args{'Name'} );
+    my $CFs = RT::CustomFields->new( $self->CurrentUser );
+    $CFs->SetContextObject( $self->ContextObject );
+    my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
+    $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
     # Don't limit to queue if queue is 0.  Trying to do so breaks
     # RT::Group type CFs.
-    if (defined $args{'Queue'}) {
-       $CFs->LimitToQueue( $args{'Queue'} );
+    if ( defined $args{'Queue'} ) {
+        $CFs->LimitToQueue( $args{'Queue'} );
     }
 
-    # When loading by name, it's ok if they're disabled. That's not a big deal.
-    $CFs->{'find_disabled_rows'}=1;
+    # When loading by name, we _can_ load disabled fields, but prefer
+    # non-disabled fields.
+    $CFs->FindAllRows;
+    $CFs->OrderByCols(
+        { FIELD => "Disabled", ORDER => 'ASC' },
+    );
 
     # We only want one entry.
     $CFs->RowsPerPage(1);
-    unless ($CFs->First) {
-        return(0);
-    }
-    return($self->Load($CFs->First->id));
 
+    # version before 3.8 just returns 0, so we need to test if wantarray to be
+    # backward compatible.
+    return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
+
+    return $self->LoadById( $first->id );
 }
 
 # }}}
 
 # {{{ Dealing with custom field values 
 
-=begin testing
-
-use_ok(RT::CustomField);
-ok(my $cf = RT::CustomField->new($RT::SystemUser));
-ok(my ($id, $msg)=  $cf->Create( Name => 'TestingCF',
-                                 Queue => '0',
-                                 SortOrder => '1',
-                                 Description => 'A Testing custom field',
-                                 Type=> 'SelectSingle'), 'Created a global CustomField');
-ok($id != 0, 'Global custom field correctly created');
-ok ($cf->SingleValue);
-is($cf->Type, 'Select');
-is($cf->MaxValues, 1);
-
-my ($val, $msg) = $cf->SetMaxValues('0');
-ok($val, $msg);
-is($cf->Type, 'Select');
-is($cf->MaxValues, 0);
-ok(!$cf->SingleValue );
-ok(my ($bogus_val, $bogus_msg) = $cf->SetType('BogusType') , "Trying to set a custom field's type to a bogus type");
-ok($bogus_val == 0, "Unable to set a custom field's type to a bogus type");
-
-ok(my $bad_cf = RT::CustomField->new($RT::SystemUser));
-ok(my ($bad_id, $bad_msg)=  $cf->Create( Name => 'TestingCF-bad',
-                                 Queue => '0',
-                                 SortOrder => '1',
-                                 Description => 'A Testing custom field with a bogus Type',
-                                 Type=> 'SelectSingleton'), 'Created a global CustomField with a bogus type');
-ok($bad_id == 0, 'Global custom field correctly decided to not create a cf with a bogus type ');
-
-=end testing
+
+=head2 Custom field values
+
+=head3 Values FIELD
+
+Return a object (collection) of all acceptable values for this Custom Field.
+Class of the object can vary and depends on the return value
+of the C<ValuesClass> method.
 
 =cut
 
-# {{{ AddValue
+*ValuesObj = \&Values;
 
-=head2 AddValue HASH
+sub Values {
+    my $self = shift;
 
-Create a new value for this CustomField.  Takes a paramhash containing the elements Name, Description and SortOrder
+    my $class = $self->ValuesClass || 'RT::CustomFieldValues';
+    eval "require $class" or die "$@";
+    my $cf_values = $class->new( $self->CurrentUser );
+    # if the user has no rights, return an empty object
+    if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
+        $cf_values->LimitToCustomField( $self->Id );
+    }
+    return ($cf_values);
+}
 
-=begin testing
+# {{{ AddValue
 
-ok(my $cf = RT::CustomField->new($RT::SystemUser));
-$cf->Load(1);
-ok($cf->Id == 1);
-ok(my ($val,$msg)  = $cf->AddValue(Name => 'foo' , Description => 'TestCFValue', SortOrder => '6'));
-ok($val != 0);
-ok (my ($delval, $delmsg) = $cf->DeleteValue($val));
-ok ($delval,"Deleting a cf value: $delmsg");
+=head3 AddValue HASH
 
-=end testing
+Create a new value for this CustomField.  Takes a paramhash containing the elements Name, Description and SortOrder
 
 =cut
 
 sub AddValue {
-       my $self = shift;
-       my %args = ( Name => undef,
-                    Description => undef,
-                    SortOrder => undef,
-                    @_ );
+    my $self = shift;
+    my %args = @_;
 
-    unless ($self->CurrentUserHasRight('AdminCustomField')) {
+    unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) {
         return (0, $self->loc('Permission Denied'));
     }
 
-    unless ($args{'Name'}) {
-        return(0, $self->loc("Can't add a custom field value without a name"));
+    # allow zero value
+    if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
+        return (0, $self->loc("Can't add a custom field value without a name"));
     }
-       my $newval = RT::CustomFieldValue->new($self->CurrentUser);
-       return($newval->Create(
-                    CustomField => $self->Id,
-             Name =>$args{'Name'},
-             Description => ($args{'Description'} || ''),
-             SortOrder => ($args{'SortOrder'} || '0')
-        ));    
+
+    my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
+    return $newval->Create( %args, CustomField => $self->Id );
 }
 
 
@@ -383,188 +433,128 @@ sub AddValue {
 
 # {{{ DeleteValue
 
-=head2 DeleteValue ID
+=head3 DeleteValue ID
 
-Deletes a value from this custom field by id. 
+Deletes a value from this custom field by id.
 
-Does not remove this value for any article which has had it selected   
+Does not remove this value for any article which has had it selected
 
 =cut
 
 sub DeleteValue {
-       my $self = shift;
+    my $self = shift;
     my $id = shift;
-    unless ($self->CurrentUserHasRight('AdminCustomField')) {
+    unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) {
         return (0, $self->loc('Permission Denied'));
     }
 
-       my $val_to_del = RT::CustomFieldValue->new($self->CurrentUser);
-       $val_to_del->Load($id);
-       unless ($val_to_del->Id) {
-               return (0, $self->loc("Couldn't find that value"));
-       }
-       unless ($val_to_del->CustomField == $self->Id) {
-               return (0, $self->loc("That is not a value for this custom field"));
-       }
+    my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
+    $val_to_del->Load( $id );
+    unless ( $val_to_del->Id ) {
+        return (0, $self->loc("Couldn't find that value"));
+    }
+    unless ( $val_to_del->CustomField == $self->Id ) {
+        return (0, $self->loc("That is not a value for this custom field"));
+    }
 
-       my $retval = $val_to_del->Delete();
-    if ($retval) {
-        return ($retval, $self->loc("Custom field value deleted"));
-    } else {
-        return(0, $self->loc("Custom field value could not be deleted"));
+    my $retval = $val_to_del->Delete;
+    unless ( $retval ) {
+        return (0, $self->loc("Custom field value could not be deleted"));
     }
+    return ($retval, $self->loc("Custom field value deleted"));
 }
 
 # }}}
 
-# {{{ Values
-
-=head2 Values FIELD
 
-Return a CustomFieldeValues object of all acceptable values for this Custom Field.
+=head2 ValidateQueue Queue
 
+Make sure that the queue specified is a valid queue name
 
 =cut
 
-*ValuesObj = \&Values;
-
-sub Values {
+sub ValidateQueue {
     my $self = shift;
+    my $id = shift;
 
-    my $cf_values = RT::CustomFieldValues->new($self->CurrentUser);
-    # if the user has no rights, return an empty object
-    if ($self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
-        $cf_values->LimitToCustomField($self->Id);
-    }
-    return ($cf_values);
-}
-
-# }}}
-
-# }}}
-
-# {{{ Ticket related routines
-
-# {{{ ValuesForTicket
-
-=head2 ValuesForTicket TICKET
-
-Returns a RT::ObjectCustomFieldValues object of this Field's values for TICKET.
-TICKET is a ticket id.
-
-This is deprecated -- use ValuesForObject instead.
-
-
-=cut
-
-sub ValuesForTicket {
-       my $self = shift;
-    my $ticket_id = shift;
-    
-    $RT::Logger->debug( ref($self) . " -> ValuesForTicket deprecated in favor of ValuesForObject at (". join(":",caller).")"); 
-    my $ticket = RT::Ticket->new($self->CurrentUser);
-    $ticket->Load($ticket_id);
+    return undef unless defined $id;
+    # 0 means "Global" null would _not_ be ok.
+    return 1 if $id eq '0';
 
-    return $self->ValuesForObject($ticket);
+    my $q = RT::Queue->new( $RT::SystemUser );
+    $q->Load( $id );
+    return undef unless $q->id;
+    return 1;
 }
 
-# }}}
-
-# {{{ AddValueForTicket
 
-=head2 AddValueForTicket HASH
+# {{{ Types
 
-Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
+=head2 Types 
 
-This is deprecated -- use AddValueForObject instead.
+Retuns an array of the types of CustomField that are supported
 
 =cut
 
-sub AddValueForTicket {
-       my $self = shift;
-       my %args = ( Ticket => undef,
-                 Content => undef,
-                    @_ );
-    $RT::Logger->debug( ref($self) . " -> AddValueForTicket deprecated in favor of AddValueForObject at (". join(":",caller).")");
-
-
-    my $ticket = RT::Ticket->new($self->CurrentUser);
-    $ticket->Load($args{'Ticket'});
-    return($self->AddValueForObject(Content => $args{'Content'}, Object => $ticket,@_));
-
+sub Types {
+    return (keys %FieldTypes);
 }
 
-
 # }}}
 
-# {{{ DeleteValueForTicket
+# {{{ IsSelectionType
 
-=head2 DeleteValueForTicket HASH
+=head2 IsSelectionType 
 
-Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
-
-This is deprecated -- use DeleteValueForObject instead.
+Retuns a boolean value indicating whether the C<Values> method makes sense
+to this Custom Field.
 
 =cut
 
-sub DeleteValueForTicket {
-       my $self = shift;
-       my %args = ( Ticket => undef,
-                 Content => undef,
-                    @_ );
-
-    $RT::Logger->debug( ref($self) . " -> DeleteValueForTicket deprecated in favor of DeleteValueForObject at (". join(":",caller).")"); 
-
-
-    my $ticket = RT::Ticket->new($self->CurrentUser);
-    $ticket->load($args{'Ticket'});
-    return ($self->DeleteValueForObject(Object => $ticket, Content => $args{'Content'}, @_));
+sub IsSelectionType {
+    my $self = shift;
+    my $type = @_? shift : $self->Type;
+    return undef unless $type;
 
+    $type =~ /(?:Select|Combobox|Autocomplete)/;
 }
 
 # }}}
-# }}}
-
 
-=head2 ValidateQueue Queue
 
-Make sure that the queue specified is a valid queue name
+=head2 IsExternalValues
 
 =cut
 
-sub ValidateQueue {
+sub IsExternalValues {
     my $self = shift;
-    my $id = shift;
-
-    if ($id eq '0') { # 0 means "Global" null would _not_ be ok.
-        return (1); 
-    }
-
-    my $q = RT::Queue->new($RT::SystemUser);
-    $q->Load($id);
-    unless ($q->id) {
-        return undef;
-    }
-    return (1);
-
+    my $selectable = $self->IsSelectionType( @_ );
+    return $selectable unless $selectable;
 
+    my $class = $self->ValuesClass;
+    return 0 if $class eq 'RT::CustomFieldValues';
+    return 1;
 }
 
+sub ValuesClass {
+    my $self = shift;
+    return '' unless $self->IsSelectionType;
 
-# {{{ Types
-
-=head2 Types 
-
-Retuns an array of the types of CustomField that are supported
+    my $class = $self->FirstAttribute( 'ValuesClass' );
+    $class = $class->Content if $class;
+    return $class || 'RT::CustomFieldValues';
+}
 
-=cut
+sub SetValuesClass {
+    my $self = shift;
+    my $class = shift || 'RT::CustomFieldValues';
 
-sub Types {
-       return (keys %FieldTypes);
+    if( $class eq 'RT::CustomFieldValues' ) {
+        return $self->DeleteAttribute( 'ValuesClass' );
+    }
+    return $self->SetAttribute( Name => 'ValuesClass', Content => $class );
 }
 
-# }}}
-
 
 =head2 FriendlyType [TYPE, MAX_VALUES]
 
@@ -578,9 +568,10 @@ sub FriendlyType {
 
     my $type = @_ ? shift : $self->Type;
     my $max  = @_ ? shift : $self->MaxValues;
+    $max = 0 unless $max;
 
     if (my $friendly_type = $FieldTypes{$type}[$max>2 ? 2 : $max]) {
-       return ( $self->loc( $friendly_type, $max ) );
+        return ( $self->loc( $friendly_type, $max ) );
     }
     else {
         return ( $self->loc( $type ) );
@@ -599,14 +590,6 @@ sub FriendlyTypeComposite {
 Takes a single string. returns true if that string is a value
 type of custom field
 
-=begin testing
-
-ok(my $cf = RT::CustomField->new($RT::SystemUser));
-ok($cf->ValidateType('SelectSingle'));
-ok($cf->ValidateType('SelectMultiple'));
-ok(!$cf->ValidateType('SelectFooMultiple'));
-
-=end testing
 
 =cut
 
@@ -614,12 +597,12 @@ sub ValidateType {
     my $self = shift;
     my $type = shift;
 
-    if ($type =~ s/(?:Single|Multiple)$//) {
-       $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
+    if ( $type =~ s/(?:Single|Multiple)$// ) {
+        $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
     }
 
-    if( $FieldTypes{$type}) {
-        return(1);
+    if ( $FieldTypes{$type} ) {
+        return 1;
     }
     else {
         return undef;
@@ -631,12 +614,57 @@ sub SetType {
     my $self = shift;
     my $type = shift;
     if ($type =~ s/(?:(Single)|Multiple)$//) {
-       $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
-       $self->SetMaxValues($1 ? 1 : 0);
+        $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
+        $self->SetMaxValues($1 ? 1 : 0);
     }
     $self->SUPER::SetType($type);
 }
 
+=head2 SetPattern STRING
+
+Takes a single string representing a regular expression.  Performs basic
+validation on that regex, and sets the C<Pattern> field for the CF if it
+is valid.
+
+=cut
+
+sub SetPattern {
+    my $self = shift;
+    my $regex = shift;
+
+    my ($ok, $msg) = $self->_IsValidRegex($regex);
+    if ($ok) {
+        return $self->SUPER::SetPattern($regex);
+    }
+    else {
+        return (0, $self->loc("Invalid pattern: [_1]", $msg));
+    }
+}
+
+=head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
+
+Tests if the string contains an invalid regex.
+
+=cut
+
+sub _IsValidRegex {
+    my $self  = shift;
+    my $regex = shift or return (1, 'valid');
+
+    local $^W; local $@;
+    local $SIG{__DIE__} = sub { 1 };
+    local $SIG{__WARN__} = sub { 1 };
+
+    if (eval { qr/$regex/; 1 }) {
+        return (1, 'valid');
+    }
+
+    my $err = $@;
+    $err =~ s{[,;].*}{};    # strip debug info from error
+    chomp $err;
+    return (0, $err);
+}
+
 # {{{ SingleValue
 
 =head2 SingleValue
@@ -648,7 +676,7 @@ Returns false if it accepts multiple values
 
 sub SingleValue {
     my $self = shift;
-    if ($self->MaxValues == 1) {
+    if (($self->MaxValues||0) == 1) {
         return 1;
     } 
     else {
@@ -658,7 +686,7 @@ sub SingleValue {
 
 sub UnlimitedValues {
     my $self = shift;
-    if ($self->MaxValues == 0) {
+    if (($self->MaxValues||0) == 0) {
         return 1;
     } 
     else {
@@ -668,8 +696,6 @@ sub UnlimitedValues {
 
 # }}}
 
-# {{{ sub CurrentUserHasRight
-
 =head2 CurrentUserHasRight RIGHT
 
 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
@@ -681,13 +707,44 @@ sub CurrentUserHasRight {
     my $right = shift;
 
     return $self->CurrentUser->HasRight(
-       Object => $self,
-       Right  => $right,
+        Object => $self,
+        Right  => $right,
     );
 }
 
-# }}}
+=head2 ACLEquivalenceObjects
+
+Returns list of objects via which users can get rights on this custom field. For custom fields
+these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
+
+=cut
+
+sub ACLEquivalenceObjects {
+    my $self = shift;
+
+    my $ctx = $self->ContextObject
+        or return;
+    return ($ctx, $ctx->ACLEquivalenceObjects);
+}
+
+=head2 ContextObject and SetContextObject
+
+Set or get a context for this object. It can be ticket, queue or another object
+this CF applies to. Used for ACL control, for example SeeCustomField can be granted on
+queue level to allow people to see all fields applied to the queue.
+
+=cut
 
+sub SetContextObject {
+    my $self = shift;
+    return $self->{'context_object'} = shift;
+}
+  
+sub ContextObject {
+    my $self = shift;
+    return $self->{'context_object'};
+}
+  
 # {{{ sub _Set
 
 sub _Set {
@@ -696,7 +753,7 @@ sub _Set {
     unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
         return ( 0, $self->loc('Permission Denied') );
     }
-    return ( $self->SUPER::_Set(@_) );
+    return $self->SUPER::_Set( @_ );
 
 }
 
@@ -712,16 +769,18 @@ Returns its value as a string, if the user passes an ACL check
 =cut
 
 sub _Value {
-
     my $self  = shift;
-    my $field = shift;
+    return undef unless $self->id;
 
     # we need to do the rights check
-    unless ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
-           return (undef);
+    unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
+        $RT::Logger->debug(
+            "Permission denied. User #". $self->CurrentUser->id
+            ." has no SeeCustomField right on CF #". $self->id
+        );
+        return (undef);
     }
-    return ( $self->__Value($field) );
-
+    return $self->__Value( @_ );
 }
 
 # }}}
@@ -730,62 +789,39 @@ sub _Value {
 =head2 SetDisabled
 
 Takes a boolean.
-1 will cause this custom field to no longer be avaialble for tickets.
-0 will re-enable this queue
+1 will cause this custom field to no longer be avaialble for objects.
+0 will re-enable this field.
 
 =cut
 
 # }}}
 
-sub Queue {
-    $RT::Logger->debug( ref($_[0]) . " -> Queue deprecated at (". join(":",caller).")");
-    
-    return 0;
-}
-
-sub SetQueue {
-    $RT::Logger->debug( ref($_[0]) . " -> SetQueue deprecated at (". join(":",caller).")");
-
-    return 0;
-}
-
-sub QueueObj {
-    $RT::Logger->debug( ref($_[0]) . " -> QueueObj deprecated at (". join(":",caller).")");
-
-    return undef;
-}
-
 =head2 SetTypeComposite
 
 Set this custom field's type and maximum values as a composite value
 
-
 =cut
 
 sub SetTypeComposite {
     my $self = shift;
     my $composite = shift;
-    my ($type, $max_values) = split(/-/, $composite, 2);
-    $self->SetType($type);
-    $self->SetMaxValues($max_values);
-}
-
-=head2 SetLookupType
 
-Autrijus: care to doc how LookupTypes work?
-
-=cut
+    my $old = $self->TypeComposite;
 
-sub SetLookupType {
-    my $self = shift;
-    my $lookup = shift;
-    if ($lookup ne $self->LookupType) {
-       # Okay... We need to invalidate our existing relationships
-       my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
-       $ObjectCustomFields->LimitToCustomField($self->Id);
-       $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
+    my ($type, $max_values) = split(/-/, $composite, 2);
+    if ( $type ne $self->Type ) {
+        my ($status, $msg) = $self->SetType( $type );
+        return ($status, $msg) unless $status;
+    }
+    if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
+        my ($status, $msg) = $self->SetMaxValues( $max_values );
+        return ($status, $msg) unless $status;
     }
-    $self->SUPER::SetLookupType($lookup);
+    return 1, $self->loc(
+        "Type changed from '[_1]' to '[_2]'",
+        $self->FriendlyTypeComposite( $old ),
+        $self->FriendlyTypeComposite( $composite ),
+    );
 }
 
 =head2 TypeComposite
@@ -797,7 +833,7 @@ Returns a composite value composed of this object's type and maximum values
 
 sub TypeComposite {
     my $self = shift;
-    join('-', $self->Type, $self->MaxValues);
+    return join '-', ($self->Type || ''), ($self->MaxValues || 0);
 }
 
 =head2 TypeComposites
@@ -808,7 +844,25 @@ Returns an array of all possible composite values for custom fields.
 
 sub TypeComposites {
     my $self = shift;
-    return grep !/Text-0/, map { ("$_-1", "$_-0") } $self->Types;
+    return grep !/(?:[Tt]ext|Combobox|Date|TimeValue)-0/, map { ("$_-1", "$_-0") } $self->Types;
+}
+
+=head2 SetLookupType
+
+Autrijus: care to doc how LookupTypes work?
+
+=cut
+
+sub SetLookupType {
+    my $self = shift;
+    my $lookup = shift;
+    if ( $lookup ne $self->LookupType ) {
+        # Okay... We need to invalidate our existing relationships
+        my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
+        $ObjectCustomFields->LimitToCustomField($self->Id);
+        $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
+    }
+    return $self->SUPER::SetLookupType($lookup);
 }
 
 =head2 LookupTypes
@@ -824,12 +878,14 @@ sub LookupTypes {
 }
 
 my @FriendlyObjectTypes = (
-    "[_1] objects",                # loc
-    "[_1]'s [_2] objects",         # loc
+    "[_1] objects",            # loc
+    "[_1]'s [_2] objects",        # loc
     "[_1]'s [_2]'s [_3] objects",   # loc
 );
 
-=head2 FriendlyTypeLookup
+=head2 FriendlyLookupType
+
+Returns a localized description of the type of this custom field
 
 =cut
 
@@ -838,7 +894,7 @@ sub FriendlyLookupType {
     my $lookup = shift || $self->LookupType;
    
     return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
-                  if (defined  $FRIENDLY_OBJECT_TYPES{$lookup} );
+                     if (defined  $FRIENDLY_OBJECT_TYPES{$lookup} );
 
     my @types = map { s/^RT::// ? $self->loc($_) : $_ }
       grep { defined and length }
@@ -847,6 +903,137 @@ sub FriendlyLookupType {
     return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
 }
 
+sub RecordClassFromLookupType {
+    my $self = shift;
+    my ($class) = ($self->LookupType =~ /^([^-]+)/);
+    unless ( $class ) {
+        $RT::Logger->error(
+            "Custom Field #". $self->id 
+            ." has incorrect LookupType '". $self->LookupType ."'"
+        );
+        return undef;
+    }
+    return $class;
+}
+
+sub CollectionClassFromLookupType {
+    my $self = shift;
+
+    my $record_class = $self->RecordClassFromLookupType;
+    return undef unless $record_class;
+
+    my $collection_class;
+    if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
+        $collection_class = $record_class.'Collection';
+    } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
+        $collection_class = $record_class.'es';
+    } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
+        $collection_class = $record_class.'s';
+    } else {
+        $RT::Logger->error("Can not find a collection class for record class '$record_class'");
+        return undef;
+    }
+    return $collection_class;
+}
+
+=head1 AppliedTo
+
+Returns collection with objects this custom field is applied to.
+Class of the collection depends on L</LookupType>.
+See all L</NotAppliedTo> .
+
+Doesn't takes into account if object is applied globally.
+
+=cut
+
+sub AppliedTo {
+    my $self = shift;
+
+    my ($res, $ocfs_alias) = $self->_AppliedTo;
+    return $res unless $res;
+
+    $res->Limit(
+        ALIAS     => $ocfs_alias,
+        FIELD     => 'id',
+        OPERATOR  => 'IS NOT',
+        VALUE     => 'NULL',
+    );
+
+    return $res;
+}
+
+=head1 NotAppliedTo
+
+Returns collection with objects this custom field is not applied to.
+Class of the collection depends on L</LookupType>.
+See all L</AppliedTo> .
+
+Doesn't takes into account if object is applied globally.
+
+=cut
+
+sub NotAppliedTo {
+    my $self = shift;
+
+    my ($res, $ocfs_alias) = $self->_AppliedTo;
+    return $res unless $res;
+
+    $res->Limit(
+        ALIAS     => $ocfs_alias,
+        FIELD     => 'id',
+        OPERATOR  => 'IS',
+        VALUE     => 'NULL',
+    );
+
+    return $res;
+}
+
+sub _AppliedTo {
+    my $self = shift;
+
+    my ($class) = $self->CollectionClassFromLookupType;
+    return undef unless $class;
+
+    my $res = $class->new( $self->CurrentUser );
+
+    # If CF is a Group CF, only display user-defined groups
+    if ( $class eq 'RT::Groups' ) {
+        $res->LimitToUserDefinedGroups;
+    }
+
+    $res->OrderBy( FIELD => 'Name' );
+    my $ocfs_alias = $res->Join(
+        TYPE   => 'LEFT',
+        ALIAS1 => 'main',
+        FIELD1 => 'id',
+        TABLE2 => 'ObjectCustomFields',
+        FIELD2 => 'ObjectId',
+    );
+    $res->Limit(
+        LEFTJOIN => $ocfs_alias,
+        ALIAS    => $ocfs_alias,
+        FIELD    => 'CustomField',
+        VALUE    => $self->id,
+    );
+    return ($res, $ocfs_alias);
+}
+
+=head2 IsApplied
+
+Takes object id and returns corresponding L<RT::ObjectCustomField>
+record if this custom field is applied to the object. Use 0 to check
+if custom field is applied globally.
+
+=cut
+
+sub IsApplied {
+    my $self = shift;
+    my $id = shift;
+    my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
+    $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
+    return undef unless $ocf->id;
+    return $ocf;
+}
 
 =head2 AddToObject OBJECT
 
@@ -863,23 +1050,35 @@ sub AddToObject {
     my $id = $object->Id || 0;
 
     unless (index($self->LookupType, ref($object)) == 0) {
-       return ( 0, $self->loc('Lookup type mismatch') );
+        return ( 0, $self->loc('Lookup type mismatch') );
     }
 
     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
         return ( 0, $self->loc('Permission Denied') );
     }
 
-    my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser );
+    if ( $self->IsApplied( $id ) ) {
+        return ( 0, $self->loc("Custom field is already applied to the object") );
+    }
 
-    $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id );
-    if ( $ObjectCF->Id ) {
-        return ( 0, $self->loc("That is already the current value") );
+    if ( $id ) {
+        # applying locally
+        return (0, $self->loc("Couldn't apply custom field to an object as it's global already") )
+            if $self->IsApplied( 0 );
+    }
+    else {
+        my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
+        $applied->LimitToCustomField( $self->id );
+        while ( my $record = $applied->Next ) {
+            $record->Delete;
+        }
     }
-    my ( $id, $msg ) =
-      $ObjectCF->Create( ObjectId => $id, CustomField => $self->Id );
 
-    return ( $id, $msg );
+    my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
+    my ( $oid, $msg ) = $ocf->Create(
+        ObjectId => $id, CustomField => $self->id,
+    );
+    return ( $oid, $msg );
 }
 
 
@@ -891,29 +1090,27 @@ Takes an object
 
 =cut
 
-
 sub RemoveFromObject {
     my $self = shift;
     my $object = shift;
     my $id = $object->Id || 0;
 
     unless (index($self->LookupType, ref($object)) == 0) {
-       return ( 0, $self->loc('Object type mismatch') );
+        return ( 0, $self->loc('Object type mismatch') );
     }
 
     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
         return ( 0, $self->loc('Permission Denied') );
     }
 
-    my $ObjectCF = RT::ObjectCustomField->new( $self->CurrentUser );
-
-    $ObjectCF->LoadByCols( ObjectId => $id, CustomField => $self->Id );
-    unless ( $ObjectCF->Id ) {
+    my $ocf = $self->IsApplied( $id );
+    unless ( $ocf ) {
         return ( 0, $self->loc("This custom field does not apply to that object") );
     }
-    my ( $id, $msg ) = $ObjectCF->Delete;
 
-    return ( $id, $msg );
+    # XXX: Delete doesn't return anything
+    my ( $oid, $msg ) = $ocf->Delete;
+    return ( $oid, $msg );
 }
 
 # {{{ AddValueForObject
@@ -944,17 +1141,20 @@ sub AddValueForObject {
         ContentType  => undef,
         @_
     );
-    my $obj = $args{'Object'} or return;
+    my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
 
     unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
         return ( 0, $self->loc('Permission Denied') );
     }
 
-    $RT::Handle->BeginTransaction;
+    unless ( $self->MatchPattern($args{'Content'}) ) {
+        return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
+    }
 
-    my $current_values = $self->ValuesForObject($obj);
+    $RT::Handle->BeginTransaction;
 
     if ( $self->MaxValues ) {
+        my $current_values = $self->ValuesForObject($obj);
         my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
 
         # (The +1 is for the new value we're adding)
@@ -965,19 +1165,25 @@ sub AddValueForObject {
 
         while ($extra_values) {
             my $extra_item = $current_values->Next;
-
             unless ( $extra_item->id ) {
-                $RT::Logger->crit(
-"We were just asked to delete a custom fieldvalue that doesn't exist!"
-                );
+                $RT::Logger->crit( "We were just asked to delete "
+                    ."a custom field value that doesn't exist!" );
                 $RT::Handle->Rollback();
                 return (undef);
             }
             $extra_item->Delete;
             $extra_values--;
-
         }
     }
+    # For date, we need to store Content as ISO date
+    if ($self->Type eq 'Date') {
+        my $DateObj = new RT::Date( $self->CurrentUser );
+        $DateObj->Set(
+            Format => 'unknown',
+            Value  => $args{'Content'},
+        );
+        $args{'Content'} = $DateObj->ISO;
+    }
     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
     my $val    = $newval->Create(
         ObjectType   => ref($obj),
@@ -990,7 +1196,7 @@ sub AddValueForObject {
 
     unless ($val) {
         $RT::Handle->Rollback();
-        return ($val);
+        return ($val, $self->loc("Couldn't create record"));
     }
 
     $RT::Handle->Commit();
@@ -1000,6 +1206,50 @@ sub AddValueForObject {
 
 # }}}
 
+# {{{ MatchPattern
+
+=head2 MatchPattern STRING
+
+Tests the incoming string against the Pattern of this custom field object
+and returns a boolean; returns true if the Pattern is empty.
+
+=cut
+
+sub MatchPattern {
+    my $self = shift;
+    my $regex = $self->Pattern or return 1;
+
+    return (( defined $_[0] ? $_[0] : '') =~ $regex);
+}
+
+
+# }}}
+
+# {{{ FriendlyPattern
+
+=head2 FriendlyPattern
+
+Prettify the pattern of this custom field, by taking the text in C<(?#text)>
+and localizing it.
+
+=cut
+
+sub FriendlyPattern {
+    my $self = shift;
+    my $regex = $self->Pattern;
+
+    return '' unless length $regex;
+    if ( $regex =~ /\(\?#([^)]*)\)/ ) {
+        return '[' . $self->loc($1) . ']';
+    }
+    else {
+        return $regex;
+    }
+}
+
+
+# }}}
+
 # {{{ DeleteValueForObject
 
 =head2 DeleteValueForObject HASH
@@ -1015,7 +1265,7 @@ sub DeleteValueForObject {
     my %args = ( Object => undef,
                  Content => undef,
                  Id => undef,
-                    @_ );
+             @_ );
 
 
     unless ($self->CurrentUserHasRight('ModifyCustomField')) {
@@ -1025,21 +1275,27 @@ sub DeleteValueForObject {
     my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
 
     if (my $id = $args{'Id'}) {
-       $oldval->Load($id);
+        $oldval->Load($id);
     }
     unless ($oldval->id) { 
-       $oldval->LoadByObjectContentAndCustomField(
-           Object => $args{'Object'}, 
-           Content =>  $args{'Content'}, 
-           CustomField => $self->Id,
-       );
+        $oldval->LoadByObjectContentAndCustomField(
+            Object => $args{'Object'}, 
+            Content =>  $args{'Content'}, 
+            CustomField => $self->Id,
+        );
     }
 
 
-    # check ot make sure we found it
+    # check to make sure we found it
     unless ($oldval->Id) {
         return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
     }
+
+    # for single-value fields, we need to validate that empty string is a valid value for it
+    if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
+        return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
+    }
+
     # delete it
 
     my $ret = $oldval->Delete();
@@ -1052,26 +1308,26 @@ sub DeleteValueForObject {
 
 =head2 ValuesForObject OBJECT
 
-Return an RT::ObjectCustomFieldValues object containing all of this custom field's values for OBJECT 
+Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT 
 
 =cut
 
 sub ValuesForObject {
-       my $self = shift;
+    my $self = shift;
     my $object = shift;
 
-       my $values = new RT::ObjectCustomFieldValues($self->CurrentUser);
-       unless ($self->CurrentUserHasRight('SeeCustomField')) {
+    my $values = new RT::ObjectCustomFieldValues($self->CurrentUser);
+    unless ($self->CurrentUserHasRight('SeeCustomField')) {
         # Return an empty object if they have no rights to see
         return ($values);
     }
-       
-       
-       $values->LimitToCustomField($self->Id);
-       $values->LimitToEnabled();
+    
+    
+    $values->LimitToCustomField($self->Id);
+    $values->LimitToEnabled();
     $values->LimitToObject($object);
 
-       return ($values);
+    return ($values);
 }
 
 
@@ -1081,10 +1337,10 @@ Tell RT that a certain object accepts custom fields
 
 Examples:
 
-    'RT::Queue-RT::Ticket'                 => "Tickets",               # loc
-    'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",   # loc
-    'RT::User'                             => "Users",                 # loc
-    'RT::Group'                            => "Groups",                        # loc
+    'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
+    'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
+    'RT::User'                             => "Users",                  # loc
+    'RT::Group'                            => "Groups",                 # loc
 
 This is a class method. 
 
@@ -1099,6 +1355,122 @@ sub _ForObjectType {
 
 }
 
-# }}}
+
+=head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
+
+Gets or sets the  C<IncludeContentForValue> for this custom field. RT
+uses this field to automatically include content into the user's browser
+as they display records with custom fields in RT.
+
+=cut
+
+sub SetIncludeContentForValue {
+    shift->IncludeContentForValue(@_);
+}
+sub IncludeContentForValue{
+    my $self = shift;
+    $self->_URLTemplate('IncludeContentForValue', @_);
+}
+
+
+
+=head2 LinkValueTo [VALUE] (and SetLinkValueTo)
+
+Gets or sets the  C<LinkValueTo> for this custom field. RT
+uses this field to make custom field values into hyperlinks in the user's
+browser as they display records with custom fields in RT.
+
+=cut
+
+
+sub SetLinkValueTo {
+    shift->LinkValueTo(@_);
+}
+
+sub LinkValueTo {
+    my $self = shift;
+    $self->_URLTemplate('LinkValueTo', @_);
+
+}
+
+
+=head2 _URLTemplate  NAME [VALUE]
+
+With one argument, returns the _URLTemplate named C<NAME>, but only if
+the current user has the right to see this custom field.
+
+With two arguments, attemptes to set the relevant template value.
+
+=cut
+
+sub _URLTemplate {
+    my $self          = shift;
+    my $template_name = shift;
+    if (@_) {
+
+        my $value = shift;
+        unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
+            return ( 0, $self->loc('Permission Denied') );
+        }
+        $self->SetAttribute( Name => $template_name, Content => $value );
+        return ( 1, $self->loc('Updated') );
+    } else {
+        unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
+            return (undef);
+        }
+
+        my @attr = $self->Attributes->Named($template_name);
+        my $attr = shift @attr;
+
+        if ($attr) { return $attr->Content }
+
+    }
+}
+
+sub SetBasedOn {
+    my $self = shift;
+    my $value = shift;
+
+    return $self->DeleteAttribute( "BasedOn" )
+        unless defined $value and length $value;
+
+    my $cf = RT::CustomField->new( $self->CurrentUser );
+    $cf->Load( ref $value ? $value->Id : $value );
+
+    return (0, "Permission denied")
+        unless $cf->Id && $cf->CurrentUserHasRight('SeeCustomField');
+
+    return $self->AddAttribute(
+        Name => "BasedOn",
+        Description => "Custom field whose CF we depend on",
+        Content => $cf->Id,
+    );
+}
+
+sub BasedOnObj {
+    my $self = shift;
+    my $obj = RT::CustomField->new( $self->CurrentUser );
+
+    my $attribute = $self->FirstAttribute("BasedOn");
+    $obj->Load($attribute->Content) if defined $attribute;
+    return $obj;
+}
+
+sub UILocation {
+    my $self = shift;
+    my $tag = $self->FirstAttribute( 'UILocation' );
+    return $tag ? $tag->Content : '';
+}
+
+sub SetUILocation {
+    my $self = shift;
+    my $tag = shift;
+    if ( $tag ) {
+        return $self->SetAttribute( Name => 'UILocation', Content => $tag );
+    }
+    else {
+        return $self->DeleteAttribute('UILocation');
+    }
+}
 
 1;