Every so often, you’ll have certain things you’d like to configure site-wide. Not quite a feature flag as we don’t want to control a full feature, but something helpful to be able to let admins control. They might cover many aspects of the system, and you might have a few, too, so holding them together in one place is helpful.
You could use environment variables for some of these (and sometimes that might be appropriate), but that can be error-prone (what if you set the wrong type for a value?) and restrictive for who might want to change the value.
With Administrate and some model tricks, we can implement a dashboard accessible to admin users and lean on the singleton pattern to provide a nice way to access and allow us to validate configuration values.
A configuration singleton
A singleton is a class we can have only one instance of, so calling it will give us back the same object each time (creating it, if necessary, the first time). In this case, we’ll have only one record and referring to it will mean we retrieve the same record each time.
There are some tradeoffs with singletons, as their global nature can make things hard to test because they couple state in different areas of code, but for occasional use, it’s a helpful pattern.
To start with, a migration:
class CreateConfigurations < ActiveRecord::Migration[7.0]
def change
create_table :configurations do |t|
t.integer :example_attribute, default: 45, null: false
t.timestamps
end
end
end
We want a default value here to simplify our model later on. Ensuring it can’t be null avoids some error cases too. Then, our model:
class Configuration < ApplicationRecord
before_create :check_for_existing
before_destroy :check_for_existing
def self.load
config = Configuration.first
if config.nil?
config = Configuration.create
end
config
end
private
def check_for_existing
raise ActiveRecord::RecordInvalid if Configuration.count >= 1
end
end
Of course, a test:
require "rails_helper"
FactoryBot.define do
factory :configuration do
example_attribute { 45 }
end
end
RSpec.describe Configuration do
it "has a valid factory" do
expect(build(:configuration)).to be_valid
end
it "does not allow another record to be created" do
create(:configuration)
config_2 = build(:configuration)
expect(config_2.save).to be_falsey
end
it "does not allow destruction of the record" do
config = create(:configuration)
expect do
config.destroy
end.to raise_error(ActiveRecord::RecordInvalid)
end
describe ".load" do
context "when a configuration record exists" do
it "returns it" do
config = create(:configuration)
expect(described_class.load).to eq(config)
end
end
context "when a configuration doesn't yet exist" do
it "creates and returns a new one" do
config = described_class.load
expect(config).to be_a(Configuration)
end
end
end
end
We end up with a database table called configurations
, then our matching
Configuration model. We create a class method to facilitate loading, which
ensures we have a record, then lean on before_create
/before_destroy
to
ensure we don’t accidentally create another record (or destroy our current
one).
Creating an Administrate dashboard
Assuming you’ve already configured administrate, we can create a new dashboard:
rails g administrate:dashboard configuration
The dashboard, once trimmed down, is very conventional. We just override the
display_resource
method to always show “Configuration” as a string:
require "administrate/base_dashboard"
class ConfigurationDashboard < Administrate::BaseDashboard
ATTRIBUTE_TYPES = {
id: Field::Number,
example_attribute: Field::Number,
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
COLLECTION_ATTRIBUTES = %i[
id
created_at
updated_at
].freeze
SHOW_PAGE_ATTRIBUTES = %i[
example_attribute
updated_at
].freeze
FORM_ATTRIBUTES = %i[
example_attribute
].freeze
def display_resource(configuration)
"Configuration"
end
end
In the controller we need to do a few more things:
module Admin
class ConfigurationsController < Admin::ApplicationController
def index
redirect_to admin_configuration_path(find_resource)
end
def find_resource(*)
::Configuration.load
end
end
end
Administrate controllers use find_resource
to fetch the relevant record
(usually based on the path) but here, we’ll use our .load
method. We
redirect from the index to our model, mainly to make the route helpers nicer to
use as it’s more straightforward to refer to.
Finally, set the routes:
namespace :admin do
resources :configurations, only: [:index, :show, :edit, :update]
end
I implemented this over four months before writing this, and it’s worked very well. It’s helped us provide a clear place to put the few configuration options we have; for the more complex ones, we can also provide validations. Because most of Administrate is straightforward in its implementation, implementing cases like this doesn’t become complicated.