Simple user preferences for your Rails app

In almost every project you need to save settings for the users of your application. One way to do that is simply adding more and more columns to your user table. If you did that for 10 or more settings you soon will realize that this doesn’t scale very well and the user model gets quickly a lot of attributes.
Another possibility would be to create another model, let’s call it UserPreferences, and create a User has many UserPreferences association. This is a fine solution, but I don’t like it much either because it’s not very flexible. In case you want to add another setting you need to create a migration and so on.

ActiveRecord has many cool features and one them is the ability to serialize a Ruby classes and save it away in an attribute of a model. Actually the example usage the documentation suggests is exactly about user preferences. Now, in the example they’re storing simply a hash, which is fine, but I decided I go ahead and create a class for user preferences:

class UserPreference
 
  attr_accessor :option1, :option2
 
  def initialize
    @option1 = nil
    @option2 = nil
  end
end

In order to save these user preferences in my user model, I added a column prefs:

class User < ActiveRecord::Base
  serialize :prefs, UserPreference
 
  def prefs
    # make sure we always return a UserPreference instance
    if read_attribute(:prefs).nil?
      write_attribute :prefs, UserPreference.new
      read_attribute :prefs
    else
      read_attribute :prefs
    end
  end
 
  def prefs=(val)
    write_attribute :prefs, val
  end
end

As you can see I overrode the getter and setter for the prefs attribute to be able to return always an instance of the UserPreference class. So far so good, by now you can do things like

user = User.find(1)
user.prefs.option1 = "hello world"
user.save!

If you need another setting, you simply can add an attribute to the UserPreference class and use it right away, without the need to create a migration for it.

After finishing that, I thought it would be handy to have these settings available in the user session. I’m using Authlogic, so I just added a method to the create method of the user_sessions controller:

  def create
    @user_session = UserSession.new(params[:user_session])
    @user_session.save do |result|
        [...]
        # load User Preferences into session
        load_user_preferences
        [...]
    end
  end

load_user_preferences looks like this:

  def load_user_preferences
    current_user.prefs.instance_variable_names.each do |v|
      session[v[1..-1].to_sym] = current_user.prefs.instance_variable_get v
    end
  end

Now you have super simple access to the current users settings through the session:

value = session[:option1]

In case some of the code which gets executed for a certain request changes e.g. session[:option1], I added an after_filter which looks like this:

  def save_user_prefs_if_changed
    changed_something = false
    current_user.prefs.instance_variable_names.each do |v|
      key = v[1..-1].to_sym
      unless current_user.prefs.instance_variable_get(v) == session[key]
        current_user.prefs.instance_variable_set v, session[key]
        changed_something = true
      end
    end unless current_user.nil?
 
    current_user.save! if changed_something && current_user
  end

This ensures that changed values in the session do get saved to the users preferences and are available when the user signs in again.

I like this approach because of its flexibility and usage of the session. It’s not ideal when a preference contains a lot of data that you don’t need often. You then wouldn’t like to store that in the session. In that case I was thinking about introducing a naming convention to prevent the preference from being stored in the session. For ListKungFu.com however I didn’t run into this situation yet.

DeliciousTwitterFacebookLinkedInRedditSlashdotTechnorati FavoritesDiggShare
This entry was posted in RubyOnRails. Bookmark the permalink.

7 Responses to Simple user preferences for your Rails app

  1. Marius Nuennerich says:


    Nice example, thanks!
    I don’t think you need to initialize option1 and option2 in the UserPreferences#initialize method to nil though. Maybe you could just let this class inherit from Struct or OpenStruct.

    Good to see you doing more Rails stuff :D

  2. skarabaeus says:


    Thanks for the comment!

    I don’t have to initialize the instance variables, but I wanted to make sure they do exist in any case and afaik instance variables exist only from the point of the first assignment. Initializing them in the constructor was the only way I saw to do that. Probably there is a more elegant way :-)

    Yeah, I’m glad to do Rails stuff whenever I have time. It’s a nice distraction from the Microsoft world I’m in for the rest of the day ;-)

  3. Mr BBQ says:


    Nice tutorial, thanks for posting this!

    Works like a charm, the one thing I’m wondering though is how to access these options nicely using the form helpers in views? Is there a possibility to do so without writing accessors?

    i.e. something like:
    bla_path do |f| %>

    Thanks for your input!

  4. skarabaeus says:


    Haven’t tried that yet to be honest. I would probably start with adding accessors. You probably could add your own form helper which generates a form based on the fields of the UserPreference class?

  5. Daniel R says:


    Excellent ! Thank you so much, that’s exactly what I needed :)
    Works like a charm.


  6. Hey Stefan,

    Fantastic writeup – this is exactly what I need for my website.

    However, I am new to Ruby and Rails and am wondering where you setup the first class in your example “class UserPreference”.

    Cheers,

    Damien


  7. Hi Damien,

    either add the class to the model directory or create a new directory. If you are using Rails 3 you can add the directory to the load path like this: http://stackoverflow.com/questions/3356742/best-way-to-load-module-class-from-lib-folder-in-rails-3

Leave a Reply

Your email address will not be published. Required fields are marked *

*


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>