Settings plugin

February 24th, 2006

This information is now old. A new version of the plugin can be found here with lots of instructions and examples.

I have noticed that in most of my projects I end up with a settings table of some sort that keeps track of global properties of the application that I would rather not hard code. Having done this a few times I decided to make an elegant reuseable plugin, with a sprinkling of the Ruby magic known as meta programming.

The syntax is easy. First, lets create some settings to keep track of:
Settings.admin_password = 'supersecret'
Settings.date_format = '%m %d, %Y'
Settings.foo = 'bar'
Now lets read them back:
Settings.foo            # returns 'bar'
Changing an existing setting is the same as creating a new setting:
Settings.foo = 'super duper bar'
Decide you dont want to track a particular setting anymore?
Settings.destroy :foo
Settings.foo            # Now gives a setting variable not found error.
Want a list of all the settings?
Settings.all            # returns {'admin_password' => 'super_secret', 'date_format' => '%m %d, %Y'}

How do you use it?

  1. Install the plugin.
  2. Create the schema for the settings table
  3. Migrate schema into your database
From the root of your rails app:
ruby script/plugin install http://beautifulpixel.textdriven.com/svn/plugins/settings/
ruby script/generate settings_migration
rake migrate

And that’s it, really.

Enjoy!

37 Responses to “Settings plugin”

  1. Michiel Says:

    Exactly what I needed – thanks!

  2. Alex Wayne Says:

    Glad its what you need, if you have any feature requests be sure to let me know.

  3. Sam Says:

    Great! Just what I was looking for. Could you provide an example of how set from a value in the envrionments files.

  4. Alex Wayne Says:

    The whole point of this is to keep these details out of your source code. Perhaps you could setup the defaults in a migration simply by:

    class CreateMySettings < ActiveRecord::Migration
      def self.up
        Settings.my_setting = 'foo'
        Settings.bar = 'abc123'
      end
      def self.down
        Settings.destroy :my_setting
        Settings.destroy :bar
      end
    end
    

    Then, with each instance of the app you can modify the DB values directly, or in a script/console session by setting a value. Ideally however you will build a page in your app that allows you to change whats in this table.

    If you want to set them in envirmonment.rb I suggest just using a constant by do this at the bottom of the file

    MY_SETTING = ‘fandoobius’

    then it should be avaible throughout your app.

    Now, having said all that, if you really really want to have a command that updates or creates a databse record everytime your application, you can simply put the assignment statement at the bottom of environment.rb:

    Settings.my_setting = ‘fandoobius’

    But if you are going to define it there, you really don;t need to store it in the database.

  5. Sam Says:

    Good points. Thanks

  6. Orya Says:

    I really like this plugin, but I ran into a slight problem when I ran something like this:

    Users.find_all.each do |u|
      u.email(:from => Settings.admin_email)
    end

    Now lets say I have 5000 users, Rails will go to the database 5000 times asking it the same each time

    SELECT * FROM settings WHERE (var = 'admin_email')

    Solution? I was going to email you my modification which added some very light lazy loading (caching) but I didn’t find it. So I’ve posted the settings.rb file here http://orya.co.uk/pages/settings

    The change has a side effect of also allowing you to access the settings like this:

    Settings[:admin_email]
    and
    Settings[:admin_email] = 'someone@somewhere.com'
  7. Orya Says:

    Oops, that link got messed up, it’s

    http://orya.co.uk/pages/settings

  8. Alex Wayne Says:

    Very nice!

    I hadn’t thought of the case of repeated queries, but a cache makes a lot of sense. I did add a reload method however, which should wipe and refill the cache (thanks to self.all) and then return itself, so if any of the Settings values change without going through the Settings object directly then you can just use:

    Settings.reload

    or

    Settings.reload.my_setting

    Thank you for the awesome contribution!

    SVN has been updated, reinstall the plugin to get the latest version.

  9. Orya Says:

    Great stuff.

    Hadn’t thought to add the reload in there, good call.

    I can forsee using this plugin on many projects, thanks!

  10. Daniel Says:

    This is great. I haven’t used it yet, but it certainly does what I need.

    As a future enhancement, I think you could add some sort of variation to handle personal settings. Therefore, an app could have global settings and personal settings, which could add to or override the global settings. All this using a similar concept as the Userstamp plugin.

    I’m not that good in Rails/Ruby yet, but I would figure that using Userstamp as a model, it shouldn’t be too difficult.

    Thanks!

  11. Jim Nicholson Says:

    If you want to expose Settings in the view, there are several options – you can pass individual settings as variables from the model, for instance. The one I like, though, is a simple helper in application_helper.rb that looks like this:

    def get_settings(name)

    Settings[name]

    end

    It might be nice if the plugin added some functionality to do this – in a better way, if there is one, certainly.

  12. Alex Wayne Says:

    Nothing wrong with asking the Settings class directly in the view, in fact it does the exact same thing as you helper without that 3 lines of code.

    <%= Settings.foolicious %>
  13. Cheap phentermine Says:

    As a future enhancement, I think you could add some sort of variation to handle personal settings. Therefore, an app could have global settings and personal settings, which could add to or override the global settings. All this using a similar concept as the Userstamp plugin.

  14. Andreas Says:

    Is it intentional that updating a setting adds a new record instead of just updating the value?

    Settings.foo = ‘bar’ Settings.foo = ‘foobar’

    This ends up as two records in my DB. id=1, var=foo, value=bar id=2, var=foo, value=foobar

    Andreas

  15. Alex Wayne Says:

    Sorry about that, new version posted on the svn repository. It should solve your problems. If its still broken in anyway please let me know.

  16. Alex Wayne Says:

    I am unfamiliar with the Userstamp plugin and probably won’t get to adding this functionality myself. But anyone is welcome to make some changes and send me a patch.

  17. Jeroen van Doorn Says:

    Hi Alex,

    I’ve tried your plugin the way you descried it, but when I do Settings.foo in one of my controllers, it gives me the following error

    “uninitialized constant Settings”

    b.t.w. I’ve tried to put your code in one of my models, but then I’m getting:

    stack level too deep #{RAILS_ROOT}/app/models/application_setting.rb:57:in `[]’ #{RAILS_ROOT}/app/models/application_setting.rb:24:in `method_missing’ etc etc…

    I’m a newbie b.t.w., so forgive me when the answers are self-evident.

    Regards, Jeroen

  18. Alex Wayne Says:

    Rails is not finding the plugin it seems. Did you install with the plugin script? Do you have the latest version of rails?

  19. Jeroen van Doorn Says:

    Hi Alex,

    It was indeed my version of rails … was still on 1.1., sorry for the inconvenience.

    Regards, Jeroen

    p.s. Nice blog!

  20. guatemala Says:

    Thank you. You have helped someone more than you could know.

  21. Rafael Lima Says:

    I would like to have default settings like this plugin have but I want to have the same settings to my Account model and User model.

    Then when I get Settings.foo, for exemple, the plugin check if the user has the foo settings defined if not, check if the account has, and then if not, it get from the default value.

    Is it possible?

    Thanks for the great plugin

  22. Oge Says:

    You’re probably tired of hearing this, but thanks for writing this plugin.

  23. Alex Wayne Says:

    Oge: Thanks, it never gets old :)

    Rafael: That’s outside what this this plugin is meant for. I don’t think this is the solution you need.

  24. Oge Says:

    Could you please explain why the value column in the settings table always contains “- ”? I am trying to create scaffolding for editing the settings table and this “feature” kind of gets in the way atimes.

  25. Alex Wayne Says:

    It’s encoded YAML. This was done so any ruby object can be stored, like Arrays and Floats, rather than only Strings. If you are setting the field manually use some_obj.to_yaml for setting the value and YAML::load(som_obj.value) for reading the value.

    And if all else fails look at the code for plugin, you should see what’s happening with YAML encoding.

  26. Pete Lasko Says:

    Hi Alex,

    This is possible a repeat question (Rafael), but looking at your code, and not fully understanding all of it, I wondered your opinion of someone extending it to a point where it would support user settings as well as general settings myuser.settings.autologin = ‘on’, for example.

    Would I be better off starting over or extending your class?

  27. Alex Wayne Says:

    You are welcome to try and extend the Settings class. Although I won’t incorporate this feature into this version of the plugin. Maybe a different version can be released called UserSettings or something, but I want to keep this plugin unbloated.

    With an added user_id it shouldnt be too hard to extend. However, I’m not what the best syntax to set and retrieve settings is since sessions are only available in controllers/views and the model will have no way to know who is logged in. Perhaps you could set a class variable for the Settings object in an app wide before filter?

    If you want to roll your own and want use the same Settings.some_foo syntax, study up on how method_missing works, because that is where all the magic happens.

    Here’s the basic rundown of what happens when you retrieve a setting.

    Settings.foo
    1. method foo is called on the class object Settings.
    2. method is not found, so Settings.method_missing(:foo) is called.
    3. inside method_missing, self[:foo] is called which triggers the [](:foo) method.
    4. the [] method finds the database record, inflates it from the YAML, and then return the result.

    Not too complicated once you understand how method missing works.

  28. gus_ Says:

    Hi have a problem with rake migrate, it trhow: table “comments” already exists

    Perhaps I don´t have some plugin not installed?

  29. Alex Wayne Says:

    That doesn’t sound like it has anything to do with the settings plugin because it doesn’t make a table called comments. You clearly have something else going on. Looks like you are trying to create a table called ‘comments’ twice and the DB doesn’t like that.

  30. gus_ Says:

    Problem solved. I’m newbie so I didnt know about migrations. Rake migrate was trying to run scripts of table creation already existent in the db/migrate directory. I leave only the add_settings table and works fine. Thanks. (Excuse my english)

  31. Steve Jernigan Says:
    The following changes are suggested to enable access to the created_at and updated_at values. First, the updated_at was not being maintained for me so I changed the update_all command in the self.[]= method to: (perhaps there is a better solution than this)
     if (update_all(['value = ?, updated_at = ?',value,Time.now], ['var = ?',var_name]) > 0)...
    
    

    Next I extracted the find out of the self.[] and then reused it in two new methods (get_created_at and get_updated_at). The following four methods should replace the self.[] method.

      def self.get_record(var_name)
        var_name = var_name.to_s
        if var = find(:first, :conditions => ['var = ?', var_name])    
            @cache[var_name] = var.value
            var
        else
            nil
        end
      end
    
      def self.get_created_at(var_name)
        (var = get_record(var_name)) ? var.created_at : nil
      end
    
      def self.get_updated_at(var_name)
        (var = get_record(var_name)) ? var.updated_at : nil
      end
    
      def self.[](var_name)
        #retrieve a setting
        var_name = var_name.to_s
    
        return @cache[var_name] if @cache[var_name] #return cached value
    
        (var = get_record(var_name)) ? var.value : nil
      end
    
    
  32. Steve Jernigan Says:

    Sorry, I just realized that I don’t have the latest version so some of the previous comment needs to be mapped to the new YAML versions. The basic idea is the same though.

  33. Alex Wayne Says:

    I have adressed this issue in a slightly different way. Adding class level methods for getting the created_at and updated_at seems to break an object oriented design pattern for me.

    So instead, if you want other accessible attributes than just the value, you can now use the Settings.object(var_name) method:

    Settings.foo #=> 'bar'
    foo = Settings.object(:foo)
    foo.value #=> 'bar'
    foo.updated_at #=> 2006-10-09 14:14:14
    foo.value = 42
    foo.save
    Settings.foo #=> 42

    So if you need more than just the value, simply instantiate the setting object. This will also allow you to add your own fields to setting object if you desire.

    Thanks for pointing out this functionality hole.

  34. Joshaven Potter Says:

    I add this to my ApplicationHelper for an added feature.

    def setting_for(setting, default = {})
        Settings[setting] || default
    end

    Then, in my view, I have the option to specify a default if the setting does not exist: setting_for(‘quick_find_frequency’, ‘3’)

  35. Alex Wayne Says:

    It seems a bit more complex that it needs to be, though. Why not just do this directly in your view?

    <%= Settings.foo || 'bar' %>

    It seems, clearer and shorter. Either way you have to specify the settings and a default.

  36. Diego Algorta Casamayou Says:

    defaults in environment.rb (rails 1.2.3 and rails edge) don’t work for me.

    It seems plugins are loaded BEFORE environment.rb.

    So I fixed it.

    Here’s the patch:

    (don’t know hoy to make it show correctly formatted here, sorry)

    Index: lib/settings.rb
    ===================================================================
    --- lib/settings.rb    (revisión: 151)
    +++ lib/settings.rb    (copia de trabajo)
    @@ -1,5 +1,6 @@
    -class Settings < ActiveRecord::Base
    
    -  @@defaults  = (defined?(SettingsDefaults) ? SettingsDefaults::DEFAULTS : {}).with_indifferent_access
    
    +class Settings < ActiveRecord::Base
    +  # This doesn't work (at least in Rails Edge r6518).
    
    +  #@@defaults  = (defined?(SettingsDefaults) ? SettingsDefaults::DEFAULTS : {}).with_indifferent_access
    
       class SettingNotFound < RuntimeError; end
    
    @@ -43,7 +44,9 @@
       end
    
       #retrieve a setting value by [] notation
    
    -  def self.[](var_name)
    
    +  def self.[](var_name)
    +    # Ensure defaults are readed. Just once.
    
    +    @@defaults ||= (defined?(SettingsDefaults) ? SettingsDefaults::DEFAULTS : {}).with_indifferent_access
    
         #retrieve a setting
    
         var_name = var_name.to_s
    
    
    ==============================
  37. Alex Wayne Says:

    Thanks Diego, I have rolled in your patch to the plugin trunk!.

Leave a Reply