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.