views:

119

answers:

2

Hi All,

I'm looking for a mechanism by which to facilitate user preferences. I also want to have a set of "master" prefs that are used if the currently logged in user doesn't have a specific pref set. I see several questions similar to this, but they seem to get into theory instead of simply proposing a quality solution.

Basically I'm looking for input on management as well as storage -- models, controllers, etc. Initially I was considering simply going with a normalized table of 50+ columns (for performance etc.). However, I plan on adding various, unknown preferences in the future and, performance aside, I could imagine multiple columns getting out of hand. Thoughts?

+1  A: 

If you don't need to manipulate or sort by individual preferences in the database, then you might want to use a single bitmask (integer) column. Basically, a bitmask is a set of on/off switches represented as a binary number. For example, let's say we have three preferences:

  1. view subscriptions
  2. view colors
  3. view full names

Let's say a user has 1 and 3 on and 2 off. Using 1s for on and 0s for off, the bitmask for this is:

101

(on off on)

This gets stored in the database as 5 because 101 is 5 in binary. Bitmasks are easy to store in the database (use a single integer column) and are easy to manipulate once you know the operators (for merging a user's preferences into the site defaults). Ryan Bates has a great tutorial on using bitmasks in Rails: Emmbedded Association. Hopefully that will give you the concrete example you're looking for.

Alex Reisner
I'm a huge fan of using bitmasks. Unfortunately, many of my prefs aren't boolean. There are color selectors, date options, quantities, etc. Any other thoughts?Thanks btw.
humble_coder
For non-boolean values you can use a serialized hash by creating a 'text' DB column called 'preferences' and adding 'serialize :preferences' to your model.
Alex Reisner
A: 

In my mind, the best way to define and use defaults is to add another row in the user preferences table, and load it as a class variable in your model. Then override the accessors to find the defaults if the preference hasn't been found. Something like this:

class UserPreference < ActiveRecord::Base
  # load default preferences as a class variable
  @@defaults ||= find(1).attributes

  # redefine accessors or each column to load default if nil
  column_names.each do |column|
    method_name = "#{column}_with_default".to_sym
    send :define_method, method_name do 
      value = send("#{column_without_default}") 
      case value
      when nil
        @@defaults[column]
      else 
        value
      end
    end
    alias_method_chain column, :default
  end
  ...
end

Essentially the default preferences (loaded from row 1) are stored in the Model as a class variable. All the accessors are redefined and made part of an alias method chain so that the default would be returned if the returned value was nil. I wanted to use || instead of case, but that would cause problems in the event that the user had set a boolean preference to false.

Edit: N.B. I don't know of a good way to update the defaults in a rails app without restarting the server.

EmFi