Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Creating strongly-typed, app-wide, user-editable settings

I recently worked on an app where an admin user needed to be able to tweak an app-wide configuration settings. For example the default title for HTML pages, or the default commission for newly hired sales people. Some settings were text values, some dates, some numbers, and all had different validations.

In this post I’ll explain how you can easily solve this problem using unique STI (Single-Table-Inheritance).

Create the base model / migration

class ApplicationSetting
end

class CreateApplicationSettings < ActiveRecord::Migration
  def self.up
    create_table :application_settings do |t|
      t.string :type, :null => false
      t.string :value
    end

    add_index :application_settings, :type, :unique => true
  end
end

Notice that the type column, which indicates that the ActiveRecord model should use Single-Table-Inheritance, has a
unique constraint. That means that you can only have 1 row per type, which means that there will only ever be a single
instance of a given settings class.

The models

Let’s say the first application setting is the default page title, which is a String.

# app/models/application_settings/default_page_title.rb

class ApplicationSettings::DefaultPageTitle < ApplicationSetting
  validates :title, :presence => true, :length => {:maximum => 50}

  def self.get
    first || create!(:title => "Welcome to acme.com")
  end

  def title() value end
  def title=(value) self.value = value end

end

There are a few noteworthy concepts in this model. The first is that it aliases the value column to be something more
descriptive (title). The second is that there
is a validation on the aliased method, which means that you’ll get a more friendly error message like ‘Title can’t be blank’,
as opposed to ‘Value can’t be blank’.
Finally, there is a get method, that ensures that if no record exists in the database, one is created with a
sort of meta-default value. This comes in very handy when creating forms and working with the controllers.

Next let’s store an integer value, representing the default commission percentage for newly-hired salespeople:

class ApplicationSettings::DefaultCommission < ApplicationSetting
  DEFAULT_PERCENTAGE = 10

  validates :percentage, :numericality => {
    :less_than_or_equal_to => 100,
    :greater_than_or_equal_to => 0
  }

  def self.get
    first || create!(:percentage => DEFAULT_PERCENTAGE)
  end

  def percentage() value.to_i end
  def percentage=(value) self.value = value.to_i.to_s end

end

Note how by defining a setting-specific getter (percentage) and setter (percent=) it’s easy to store all values as
strings and then coerce the value into
something that can be more easily handled by rails view helpers and validations.

You may want to store each value in a different strongly-typed column in the database (like string_value, int_value, date_value etc…) and let rails handle the type casting,
and if you did, your code would be virtually identical:

class CreateApplicationSettings < ActiveRecord::Migration
  def self.up
    create_table :application_settings do |t|
      t.string :type, :null => false
      t.string :string_value
      t.integer :int_value
      t.date :date_value
      # etc...
    end

    add_index :application_settings, :type, :unique => true
  end
end

class ApplicationSettings::DefaultCommission < ApplicationSetting
  DEFAULT_PERCENTAGE = 10

  validates :percentage, :numericality => {
    :less_than_or_equal_to => 100,
    :greater_than_or_equal_to => 0
  }

  def self.get
    first || create!(:percentage => DEFAULT_PERCENTAGE)
  end

  def percentage() int_value end
  def percentage=(value) self.int_value = value end
end

The routes

As far as the routes go, you could map to a different controller for each settings class, or map to a single controller with custom actions – depends on your preference.
Here’s an example of mapping everything to one controller:

# config/routes.rb

namespace :admin do
  resources :application_settings do
    collection do
      put :default_page_title # => PUT /admin/application_settings/default_page_title
      put :default_commission
    end
  end
end

The view

# app/views/admin/application_settings/index.html.erb

<%= form_for ApplicationSettings::DefaultPageTitle.get, :url => default_page_title_admin_application_settings_path(@default_page_title), :as => :setting do |f| %>
  <%= f.error_messages %>
  <%= f.label :title %>
  <%= f.text_field :title, :size => 50, :maxlength => 50 %>
  <%= f.submit "Save" %>
<% end %>

<%= form_for ApplicationSettings::DefaultCommission.get, :url => default_commission_admin_application_settings_path(@default_commission), :as => :setting do |f| %>
  <%= f.error_messages %>
  <%= f.label :percentage %>
  <%= f.text_field :percentage, :size => 3, :maxlength => 3 %>
  <%= f.submit "Save" %>
<% end %>

From the form’s point of view, each of these objects is completely separate. Notice the :as => :setting option in form_for.
This ensures that when the params get to the controller, they can be accessed with params[:setting], as opposed to
params[:application_setting_default_page_title] – that’s an important step to keeping the controller DRY.

The controller

# app/controllers/admin/application_settings_controller.rb

class Admin::ApplicationSettingsController < ApplicationController
  def default_page_title
    update_setting ApplicationSettings::DefaultPageTitle
  end

  def default_commission
    update_setting ApplicationSettings::DefaultCommission
  end

  private

  def update_setting(klass)
    setting = klass.get
    setting.update_attributes(params[:setting])
    redirect_to admin_application_settings_path
  end
end

Since each settings class conforms to the same interface (get), and the view has ensured that the params get sent up
as params[:setting] the controller becomes pretty trivial.

Usage

Anywhere you need access to the value of a setting, you just call get on the appropriate settings object. Obviously if this were
production code you could cache those settings for performance.

References

I originally learned about this modeling pattern from Dan Chak’s book Enterprise Rails.

Comments
  1. grosser says:

    Sounds like a whole lot of overhead to me, have you tried using something simpler like https://github.com/ledermann/rails-settings ?

    Just use it with a generic controller that list/updates all keys/values.

  2. Jeff Dean says:

    Thanks for the link. The point of this post was to show how you can add validations with friendly error messages for each setting. But if I need a generic key/value store in I’ll give it a try.

  3. Peter Jaros says:

    That’s pretty sweet! You might be able to avoid `:as => :setting` by overriding `.model_name`. Then you wouldn’t have to think about it when you make each form.

  4. Strange approach from maintenance stand point.
    You need work hard to support this structure.

    Getting custom validation for each attribute is not a big deal.

    Try serialization for all attributes and you won’t need migrations at all.

    “`
    class Settings < AR:B serialize :options, Hash delegate :opt1, :opt2, :to => Hash

    validate :opt1 ….
    validate :opt2
    end
    “`

    No types, no conventions. All data types allowed.

  5. Jim Kingdon says:

    It does seem like a lot of code for something where the first instinct would be something like:

    setting :default_commission_percentage, :decimal
    setting :default_page_title, :string, :default => “welcome to (your company here)”

    but given the requirements as stated (e.g. setting-specific validation), perhaps it isn’t so bad.

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *