import rt 3.4.4
[freeside.git] / rt / lib / RT / CustomField_Overlay.pm
index 4c4bd8b..9e0ce24 100644 (file)
@@ -1,8 +1,8 @@
-# {{{ BEGIN BPS TAGGED BLOCK
+# BEGIN BPS TAGGED BLOCK {{{
 # 
 # COPYRIGHT:
 #  
-# This software is Copyright (c) 1996-2004 Best Practical Solutions, LLC 
+# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC 
 #                                          <jesse@bestpractical.com>
 # 
 # (Except where explicitly superseded by other copyright notices)
 # works based on those contributions, and sublicense and distribute
 # those contributions and any derivatives thereof.
 # 
-# }}} END BPS TAGGED BLOCK
+# END BPS TAGGED BLOCK }}}
+package RT::CustomField;
+
 use strict;
 no warnings qw(redefine);
 
-use vars qw(@TYPES %TYPES);
+use vars qw(%FieldTypes $RIGHTS %FRIENDLY_OBJECT_TYPES);
 
 use RT::CustomFieldValues;
-use RT::TicketCustomFieldValues;
-
-# Enumerate all valid types for this custom field
-@TYPES = (
-    'SelectSingle',    # loc
-    'SelectMultiple',  # loc
-    'FreeformSingle',  # loc
-    'FreeformMultiple', # loc
+use RT::ObjectCustomFieldValues;
+
+
+%FieldTypes = (
+    Select => [
+        '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
+    ],
+    Text => [
+        '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 up to [_1] wikitext areas',# loc
+    ],
+    Image => [
+        '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
+    ],
 );
 
-# Populate a hash of types of easier validation
-for (@TYPES) { $TYPES{$_} = 1};
 
+%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::Group' => "Groups", );                          #loc
+
+$RIGHTS = {
+    SeeCustomField            => 'See custom fields',       # loc_pair
+    AdminCustomField          => 'Create, delete and modify custom fields',        # 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
+$RT::ACE::OBJECT_TYPES{'RT::CustomField'} = 1;
+
+foreach my $right ( keys %{$RIGHTS} ) {
+    $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
+}
+
+sub AvailableRights {
+    my $self = shift;
+    return($RIGHTS);
+}
 
 =head1 NAME
 
@@ -83,11 +133,17 @@ Create takes a hash of values and creates a row in the database:
 
   varchar(200) 'Name'.
   varchar(200) 'Type'.
-  int(11) 'Queue'.
+  int(11) 'MaxValues'.
+  varchar(255) 'Pattern'.
+  smallint(6) 'Repeated'.
   varchar(255) 'Description'.
   int(11) 'SortOrder'.
+  varchar(255) 'LookupType'.
   smallint(6) 'Disabled'.
 
+  'LookupType' is generally the result of either 
+  RT::Ticket->CustomFieldLookupType or RT::Transaction->CustomFieldLookupType
+
 =cut
 
 
@@ -98,19 +154,36 @@ sub Create {
     my %args = ( 
                 Name => '',
                 Type => '',
-                Queue => '0',
+               MaxValues => '0',
+               Pattern  => '',
                 Description => '',
-                SortOrder => '0',
                 Disabled => '0',
+               LookupType  => '',
+               Repeated  => '0',
 
                  @_);
 
-    
+    unless ($self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField')) {
+        return (0, $self->loc('Permission Denied'));
+    }
 
-    if (  ! $args{'Queue'} ) {
-        unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AdminCustomFields') ) {
+
+    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;
+    }
+    
+    if ( !exists $args{'Queue'}) {
+       # 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';
     }
     else {
         my $queue = RT::Queue->new($self->CurrentUser);
@@ -118,35 +191,75 @@ sub Create {
         unless ($queue->Id) {
             return (0, $self->loc("Queue not found"));
         }
-        unless ( $queue->CurrentUserHasRight('AdminCustomFields') ) {
+        unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
             return ( 0, $self->loc('Permission Denied') );
         }
+       $args{'LookupType'} = 'RT::Queue-RT::Ticket';
     }
-    $self->SUPER::Create(
+    my $rv = $self->SUPER::Create(
                          Name => $args{'Name'},
                          Type => $args{'Type'},
-                         Queue => $args{'Queue'},
+                         MaxValues => $args{'MaxValues'},
+                         Pattern  => $args{'Pattern'},
                          Description => $args{'Description'},
-                         SortOrder => $args{'SortOrder'},
                          Disabled => $args{'Disabled'},
+                        LookupType => $args{'LookupType'},
+                        Repeated => $args{'Repeated'},
 );
 
+    return $rv unless exists $args{'Queue'};
+
+    # Compat code -- create a new ObjectCustomField mapping
+    my $OCF = RT::ObjectCustomField->new($self->CurrentUser);
+    $OCF->Create(
+       CustomField => $self->Id,
+       ObjectId => $args{'Queue'},
+    );
+
+    return $rv;
+}
+
+=head2 Load ID/NAME
+
+Load a custom field.  If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
+
+=cut
+
+
+sub Load {
+    my $self = shift;
+    my $id = shift;
+
+    if ($id =~ /^\d+$/) {
+        return ($self->SUPER::Load($id));
+    } else {
+        return($self->LoadByName(Name => $id));
+    }
 }
 
 
-# {{{ sub LoadByNameAndQueue
+# {{{ sub LoadByName
+
+=head2  LoadByName (Queue => QUEUEID, Name => NAME)
+
+Loads the Custom field named 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.
 
-=head2  LoadByNameAndQueue (Queue => QUEUEID, Name => NAME)
+If no queue parameter is specified, look for any and all custom fields with this name.
 
-Loads the Custom field named NAME for Queue QUEUE. If QUEUE is 0,
-loads a global custom field
+BUG/TODO, this won't let you specify that you only want user or group CFs.
 
 =cut
 
 # Compatibility for API change after 3.0 beta 1
-*LoadNameAndQueue = \&LoadByNameAndQueue;
+*LoadNameAndQueue = \&LoadByName;
+# Change after 3.4 beta.
+*LoadByNameAndQueue = \&LoadByName;
 
-sub LoadByNameAndQueue {
+sub LoadByName {
     my $self = shift;
     my %args = (
         Queue => undef,
@@ -154,13 +267,33 @@ sub LoadByNameAndQueue {
         @_,
     );
 
-    if ($args{'Queue'} =~ /\D/) {
+    # 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;
     }
 
-    return ( $self->LoadByCols( Name => $args{'Name'}, Queue => $args{'Queue'} ) );
+    # 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'} );
+    # 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'} );
+    }
+
+    # When loading by name, it's ok if they're disabled. That's not a big deal.
+    $CFs->{'find_disabled_rows'}=1;
+
+    # We only want one entry.
+    $CFs->RowsPerPage(1);
+    unless ($CFs->First) {
+        return(0);
+    }
+    return($self->Load($CFs->First->id));
 
 }
 
@@ -169,6 +302,7 @@ sub LoadByNameAndQueue {
 # {{{ 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',
@@ -178,10 +312,13 @@ ok(my ($id, $msg)=  $cf->Create( Name => 'TestingCF',
                                  Type=> 'SelectSingle'), 'Created a global CustomField');
 ok($id != 0, 'Global custom field correctly created');
 ok ($cf->SingleValue);
-ok($cf->Type eq 'SelectSingle');
+is($cf->Type, 'Select');
+is($cf->MaxValues, 1);
 
-ok($cf->SetType('SelectMultiple'));
-ok($cf->Type eq 'SelectMultiple');
+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");
@@ -212,7 +349,7 @@ 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 != 0);
+ok ($delval,"Deleting a cf value: $delmsg");
 
 =end testing
 
@@ -225,7 +362,7 @@ sub AddValue {
                     SortOrder => undef,
                     @_ );
 
-    unless ($self->CurrentUserHasRight('AdminCustomFields')) {
+    unless ($self->CurrentUserHasRight('AdminCustomField')) {
         return (0, $self->loc('Permission Denied'));
     }
 
@@ -257,7 +394,7 @@ Does not remove this value for any article which has had it selected
 sub DeleteValue {
        my $self = shift;
     my $id = shift;
-    unless ($self->CurrentUserHasRight('AdminCustomFields')) {
+    unless ($self->CurrentUserHasRight('AdminCustomField')) {
         return (0, $self->loc('Permission Denied'));
     }
 
@@ -289,11 +426,14 @@ Return a CustomFieldeValues object of all acceptable values for this Custom Fiel
 
 =cut
 
+*ValuesObj = \&Values;
+
 sub Values {
     my $self = shift;
 
     my $cf_values = RT::CustomFieldValues->new($self->CurrentUser);
-    if ( $self->__Value('Queue') == 0 || $self->CurrentUserHasRight( 'SeeQueue') ) {
+    # if the user has no rights, return an empty object
+    if ($self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
         $cf_values->LimitToCustomField($self->Id);
     }
     return ($cf_values);
@@ -309,21 +449,23 @@ sub Values {
 
 =head2 ValuesForTicket TICKET
 
-Returns a RT::TicketCustomFieldValues object of this Field's values for 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"); 
+    my $ticket = RT::Ticket->new($self->CurrentUser);
+    $ticket->Load($ticket_id);
 
-       my $values = new RT::TicketCustomFieldValues($self->CurrentUser);
-       $values->LimitToCustomField($self->Id);
-    $values->LimitToTicket($ticket_id);
-
-       return ($values);
+    return $self->ValuesForObject($ticket);
 }
 
 # }}}
@@ -334,6 +476,8 @@ sub ValuesForTicket {
 
 Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
 
+This is deprecated -- use AddValueForObject instead.
+
 =cut
 
 sub AddValueForTicket {
@@ -341,13 +485,12 @@ sub AddValueForTicket {
        my %args = ( Ticket => undef,
                  Content => undef,
                     @_ );
+    $RT::Logger->debug( ref($self) . " -> AddValueForTicket deprecated in favor of AddValueForObject"); 
 
-       my $newval = RT::TicketCustomFieldValue->new($self->CurrentUser);
-       my $val = $newval->Create(Ticket => $args{'Ticket'},
-                            Content => $args{'Content'},
-                            CustomField => $self->Id);
 
-    return($val);
+    my $ticket = RT::Ticket->new($self->CurrentUser);
+    $ticket->Load($args{'Ticket'});
+    return($self->AddValueForObject(Content => $args{'Content'}, Object => $ticket,@_));
 
 }
 
@@ -360,6 +503,8 @@ sub AddValueForTicket {
 
 Adds a custom field value for a ticket. Takes a param hash of Ticket and Content
 
+This is deprecated -- use DeleteValueForObject instead.
+
 =cut
 
 sub DeleteValueForTicket {
@@ -368,23 +513,14 @@ sub DeleteValueForTicket {
                  Content => undef,
                     @_ );
 
-       my $oldval = RT::TicketCustomFieldValue->new($self->CurrentUser);
-    $oldval->LoadByTicketContentAndCustomField (Ticket => $args{'Ticket'}, 
-                                                Content =>  $args{'Content'}, 
-                                                CustomField => $self->Id );
-    # check ot 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));
-    }
-    # delete it
+    $RT::Logger->debug( ref($self) . " -> DeleteValueForTicket deprecated in favor of DeleteValueForObject"); 
 
-    my $ret = $oldval->Delete();
-    unless ($ret) {
-        return(0, $self->loc("Custom field value could not be found"));
-    }
-    return(1, $self->loc("Custom field value deleted"));
-}
 
+    my $ticket = RT::Ticket->new($self->CurrentUser);
+    $ticket->load($args{'Ticket'});
+    return ($self->DeleteValueForObject(Object => $ticket, Content => $args{'Content'}, @_));
+
+}
 
 # }}}
 # }}}
@@ -424,13 +560,13 @@ Retuns an array of the types of CustomField that are supported
 =cut
 
 sub Types {
-       return (@TYPES);
+       return (keys %FieldTypes);
 }
 
 # }}}
 
 
-=head2 FriendlyType [TYPE]
+=head2 FriendlyType [TYPE, MAX_VALUES]
 
 Returns a localized human-readable version of the custom field type.
 If a custom field type is specified as the parameter, the friendly type for that type will be returned
@@ -440,25 +576,23 @@ If a custom field type is specified as the parameter, the friendly type for that
 sub FriendlyType {
     my $self = shift;
 
-    my $type = shift || $self->Type;
+    my $type = @_ ? shift : $self->Type;
+    my $max  = @_ ? shift : $self->MaxValues;
 
-    if ( $type eq 'SelectSingle' ) {
-        return ( $self->loc('Select one value') );
-    }
-    elsif ( $type eq 'SelectMultiple' ) {
-        return ( $self->loc('Select multiple values') );
-    }
-    elsif ( $type eq 'FreeformSingle' ) {
-        return ( $self->loc('Enter one value') );
-    }
-    elsif ( $type eq 'FreeformMultiple' ) {
-        return ( $self->loc('Enter multiple values') );
+    if (my $friendly_type = $FieldTypes{$type}[$max>2 ? 2 : $max]) {
+       return ( $self->loc( $friendly_type, $max ) );
     }
     else {
-        return ( $self->loc( $self->Type ) );
+        return ( $self->loc( $type ) );
     }
 }
 
+sub FriendlyTypeComposite {
+    my $self = shift;
+    my $composite = shift || $self->TypeComposite;
+    return $self->FriendlyType(split(/-/, $composite, 2));
+}
+
 
 =head2 ValidateType TYPE
 
@@ -480,7 +614,11 @@ sub ValidateType {
     my $self = shift;
     my $type = shift;
 
-    if( $TYPES{$type}) {
+    if ($type =~ s/(?:Single|Multiple)$//) {
+       $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead");
+    }
+
+    if( $FieldTypes{$type}) {
         return(1);
     }
     else {
@@ -488,6 +626,17 @@ sub ValidateType {
     }
 }
 
+
+sub SetType {
+    my $self = shift;
+    my $type = shift;
+    if ($type =~ s/(?:(Single)|Multiple)$//) {
+       warn "'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead";
+       $self->SetMaxValues($1 ? 1 : 0);
+    }
+    $self->SUPER::SetType($type);
+}
+
 # {{{ SingleValue
 
 =head2 SingleValue
@@ -499,7 +648,17 @@ Returns false if it accepts multiple values
 
 sub SingleValue {
     my $self = shift;
-    if ($self->Type =~  /Single$/) {
+    if ($self->MaxValues == 1) {
+        return 1;
+    } 
+    else {
+        return undef;
+    }
+}
+
+sub UnlimitedValues {
+    my $self = shift;
+    if ($self->MaxValues == 0) {
         return 1;
     } 
     else {
@@ -518,14 +677,13 @@ Helper function to call the custom field's queue's CurrentUserHasRight with the
 =cut
 
 sub CurrentUserHasRight {
-    my $self = shift;
+    my $self  = shift;
     my $right = shift;
-    # if there's no queue, we want to know about a global right
-    if ( ( !defined $self->__Value('Queue') ) || ( $self->__Value('Queue') == 0 ) ) {
-         return $self->CurrentUser->HasRight( Object => $RT::System, Right => $right); 
-    } else {
-        return ( $self->QueueObj->CurrentUserHasRight($right) );
-    }
+
+    return $self->CurrentUser->HasRight(
+       Object => $self,
+       Right  => $right,
+    );
 }
 
 # }}}
@@ -535,7 +693,7 @@ sub CurrentUserHasRight {
 sub _Set {
     my $self = shift;
 
-    unless ( $self->CurrentUserHasRight('AdminCustomFields') ) {
+    unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
         return ( 0, $self->loc('Permission Denied') );
     }
     return ( $self->SUPER::_Set(@_) );
@@ -558,16 +716,10 @@ sub _Value {
     my $self  = shift;
     my $field = shift;
 
-    # We need to expose the queue so that we can do things like ACL checks
-    if ( $field eq 'Queue') {
-          return ( $self->SUPER::_Value($field) );
-     }
-
-
-    #Anybody can see global custom fields, otherwise we need to do the rights check
-        unless ( $self->__Value('Queue') == 0 || $self->CurrentUserHasRight( 'SeeQueue') ) {
-            return (undef);
-        }
+    # we need to do the rights check
+    unless ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
+           return (undef);
+    }
     return ( $self->__Value($field) );
 
 }
@@ -585,4 +737,368 @@ Takes a boolean.
 
 # }}}
 
+sub Queue {
+    $RT::Logger->debug( ref($_[0]) . " -> Queue deprecated");
+    
+    return 0;
+}
+
+sub SetQueue {
+    $RT::Logger->debug( ref($_[0]) . " -> SetQueue deprecated");
+
+    return 0;
+}
+
+sub QueueObj {
+    $RT::Logger->debug( ref($_[0]) . " -> QueueObj deprecated");
+
+    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
+
+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};
+    }
+    $self->SUPER::SetLookupType($lookup);
+}
+
+=head2 TypeComposite
+
+Returns a composite value composed of this object's type and maximum values
+
+=cut
+
+
+sub TypeComposite {
+    my $self = shift;
+    join('-', $self->Type, $self->MaxValues);
+}
+
+=head2 TypeComposites
+
+Returns an array of all possible composite values for custom fields.
+
+=cut
+
+sub TypeComposites {
+    my $self = shift;
+    return grep !/Text-0/, map { ("$_-1", "$_-0") } $self->Types;
+}
+
+=head2 LookupTypes
+
+Returns an array of LookupTypes available
+
+=cut
+
+
+sub LookupTypes {
+    my $self = shift;
+    return keys %FRIENDLY_OBJECT_TYPES;
+}
+
+my @FriendlyObjectTypes = (
+    "[_1] objects",                # loc
+    "[_1]'s [_2] objects",         # loc
+    "[_1]'s [_2]'s [_3] objects",   # loc
+);
+
+=head2 FriendlyTypeLookup
+
+=cut
+
+sub FriendlyLookupType {
+    my $self = shift;
+    my $lookup = shift || $self->LookupType;
+   
+    return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
+                  if (defined  $FRIENDLY_OBJECT_TYPES{$lookup} );
+
+    my @types = map { s/^RT::// ? $self->loc($_) : $_ }
+      grep { defined and length }
+      split( /-/, $lookup )
+      or return;
+    return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
+}
+
+
+=head2 AddToObject OBJECT
+
+Add this custom field as a custom field for a single object, such as a queue or group.
+
+Takes an object 
+
+=cut
+
+
+sub AddToObject {
+    my $self  = shift;
+    my $object = shift;
+    my $id = $object->Id || 0;
+
+    unless (index($self->LookupType, ref($object)) == 0) {
+       return ( 0, $self->loc('Lookup 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 );
+    if ( $ObjectCF->Id ) {
+        return ( 0, $self->loc("That is already the current value") );
+    }
+    my ( $id, $msg ) =
+      $ObjectCF->Create( ObjectId => $id, CustomField => $self->Id );
+
+    return ( $id, $msg );
+}
+
+
+=head2 RemoveFromObject OBJECT
+
+Remove this custom field  for a single object, such as a queue or group.
+
+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') );
+    }
+
+    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 ) {
+        return ( 0, $self->loc("This custom field does not apply to that object") );
+    }
+    my ( $id, $msg ) = $ObjectCF->Delete;
+
+    return ( $id, $msg );
+}
+
+# {{{ AddValueForObject
+
+=head2 AddValueForObject HASH
+
+Adds a custom field value for a record object of some kind. 
+Takes a param hash of 
+
+Required:
+
+    Object
+    Content
+
+Optional:
+
+    LargeContent
+    ContentType
+
+=cut
+
+sub AddValueForObject {
+    my $self = shift;
+    my %args = (
+        Object       => undef,
+        Content      => undef,
+        LargeContent => undef,
+        ContentType  => undef,
+        @_
+    );
+    my $obj = $args{'Object'} or return;
+
+    unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
+        return ( 0, $self->loc('Permission Denied') );
+    }
+
+    $RT::Handle->BeginTransaction;
+
+    my $current_values = $self->ValuesForObject($obj);
+
+    if ( $self->MaxValues ) {
+        my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
+
+        # (The +1 is for the new value we're adding)
+
+        # If we have a set of current values and we've gone over the maximum
+        # allowed number of values, we'll need to delete some to make room.
+        # which former values are blown away is not guaranteed
+
+        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::Handle->Rollback();
+                return (undef);
+            }
+            $extra_item->Delete;
+            $extra_values--;
+
+        }
+    }
+    my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
+    my $val    = $newval->Create(
+        ObjectType   => ref($obj),
+        ObjectId     => $obj->Id,
+        Content      => $args{'Content'},
+        LargeContent => $args{'LargeContent'},
+        ContentType  => $args{'ContentType'},
+        CustomField  => $self->Id
+    );
+
+    unless ($val) {
+        $RT::Handle->Rollback();
+        return ($val);
+    }
+
+    $RT::Handle->Commit();
+    return ($val);
+
+}
+
+# }}}
+
+# {{{ DeleteValueForObject
+
+=head2 DeleteValueForObject HASH
+
+Deletes a custom field value for a ticket. Takes a param hash of Object and Content
+
+Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
+
+=cut
+
+sub DeleteValueForObject {
+    my $self = shift;
+    my %args = ( Object => undef,
+                 Content => undef,
+                 Id => undef,
+                    @_ );
+
+
+    unless ($self->CurrentUserHasRight('ModifyCustomField')) {
+        return (0, $self->loc('Permission Denied'));
+    }
+
+    my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
+
+    if (my $id = $args{'Id'}) {
+       $oldval->Load($id);
+    }
+    unless ($oldval->id) { 
+       $oldval->LoadByObjectContentAndCustomField(
+           Object => $args{'Object'}, 
+           Content =>  $args{'Content'}, 
+           CustomField => $self->Id,
+       );
+    }
+
+
+    # check ot 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));
+    }
+    # delete it
+
+    my $ret = $oldval->Delete();
+    unless ($ret) {
+        return(0, $self->loc("Custom field value could not be found"));
+    }
+    return($oldval->Id, $self->loc("Custom field value deleted"));
+}
+
+
+=head2 ValuesForObject OBJECT
+
+Return an RT::ObjectCustomFieldValues object containing all of this custom field's values for OBJECT 
+
+=cut
+
+sub ValuesForObject {
+       my $self = shift;
+    my $object = shift;
+
+       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->LimitToObject($object);
+
+       return ($values);
+}
+
+
+=head2 _ForObjectType PATH FRIENDLYNAME
+
+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
+
+This is a class method. 
+
+=cut
+
+sub _ForObjectType {
+    my $self = shift;
+    my $path = shift;
+    my $friendly_name = shift;
+
+    $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
+
+}
+
+# }}}
+
 1;