Robert Ross
Robert Ross

Reputation: 1925

ActiveRecord instance settings

I'm curious if there's a gem or way of doing per-model settings. For example, user preferences.

I'd like to have defaults per model (say a class attribute), but definable (on another table! I don't want a serializable field on my model).

For example:

user = User.find(1)
user.settings.newsletter # => true

There would be a model for UserSetting that has a schema of key => string, value => string, type => string (Boolean, Date, String, etc...)

UPDATE:

Here is my solution to this in the end. Supports value types for settings (Boolean, Time, etc)

    def setting(key, whiny=true)
      s = user_settings.where(:key => key).first

      if s
        case s.value_type
        when 'Boolean'
          s.value.to_i == 1
        when 'Time'
          Time.parse(s.value)
        else
          s.value
        end
      else
        if whiny
          raise NameError, "Setting key #{key} does not exist for #{name}."
        else
          nil
        end
      end
    end

Upvotes: 0

Views: 163

Answers (2)

Jordan Running
Jordan Running

Reputation: 106037

The has_settings gem appears to do this, but alas doesn't appear to be maintained. This fork has Rails 3 support, at least.

If you can't find a gem that suits your needs it's pretty straightforward to implement on your own with a simple has_many relation and a little association extension magic, e.g. (untested):

class User < ActiveRecord::Base
  has_many :settings do
    # association extension
    def method_missing name, *args
      key = name.to_s

      # setter
      if key.ends_with? '='
        key.chop!
        new_val = args.first

        setting = find_or_create_by_key key

        setting.update_attribute(:value, new_val) &&
          @settings[key] = new_val                && # KISS cache
          true  # to mirror the semantics of update_attribute

      # getter
      elsif @settings.has_key? name # is it cached?
        @settings[name]

      elsif setting = where(:key => name).select(:value).first
        @settings[name] = setting.value # KISS cache again

      else
        super
      end

    end
  end

end

class Setting < ActiveRecord::Base
  belongs_to :user

  serialize :value # so we don't have to bother with an extra :type attribute
end

class CreateSettings < ActiveRecord::Migration
  def up
    create_table :settings do |t|
      t.references :user
      t.string :key
      t.string :value
    end
  end

  def down
    remove_table :settings
  end
end

Obviously this gets the job done but obviously isn't very robust so you may encounter gotchas.

Upvotes: 0

MrTheWalrus
MrTheWalrus

Reputation: 9700

I would implement this with User has_many UserSettings, and UserSetting is user_id, key, and value. Maybe have a convenience method for accessing them, like so:

class User < ActiveRecord::Base
  has_many :user_settings
  def setting(key)
    user_settings.where(:key => key).first.try(&:value)
  end
end

class UserSetting < ActiveRecord::Base
  belongs_to :user
end

Then, you can go

user = User.find(1)
user.setting('newsletter') # => true/false (or nil if that setting doesn't exist)

Upvotes: 1

Related Questions