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 toparams[: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.
About the Author