The best solution for store global settings in Rails applications.
This gem will make managing a table of а global key, value pairs easy. Think of it like a global Hash stored in your database, that uses simple ActiveRecord like methods for manipulation. Keep track of any global setting that you don't want to hard code into your Rails application.
Edit your Gemfile:
$ bundle add rails-settings-cachedGenerate your settings:
$ rails g settings:install # Or use a custom name: $ rails g settings:install AppConfigYou will get app/models/setting.rb
class Setting < RailsSettings::Base # cache_prefix { "v1" } scope :application do field :app_name, default: "Rails Settings", validates: { presence: true, length: { in: 2..20 } } field :host, default: "http://example.com", readonly: true field :default_locale, default: "zh-CN", validates: { presence: true, inclusion: { in: %w[zh-CN en jp] } }, option_values: %w[en zh-CN jp], help_text: "Bla bla ..." field :admin_emails, type: :array, default: %w[admin@rubyonrails.org] # lambda default value field :welcome_message, type: :string, default: -> { "welcome to #{self.app_name}" }, validates: { length: { maximum: 255 } } # Override array separator, default: /[\n,]/ split with \n or comma. field :tips, type: :array, separator: /[\n]+/ end scope :limits do field :user_limits, type: :integer, default: 20 field :exchange_rate, type: :float, default: 0.123 field :captcha_enable, type: :boolean, default: true end field :notification_options, type: :hash, default: { send_all: true, logging: true, sender_email: "foo@bar.com" } field :readonly_item, type: :integer, default: 100, readonly: true endYou must use the field method to statement the setting keys, otherwise you can't use it.
The scope method allows you to group the keys for admin UI.
Now just put that migration in the database with:
$ rails db:migrateThe syntax is easy. First, let's create some settings to keep track of:
irb > Setting.host "http://example.com" irb > Setting.app_name "Rails Settings" irb > Setting.app_name = "Rails Settings Cached" irb > Setting.app_name "Rails Settings Cached" irb > Setting.user_limits 20 irb > Setting.user_limits = "30" irb > Setting.user_limits 30 irb > Setting.user_limits = 45 irb > Setting.user_limits 45 irb > Setting.captcha_enable 1 irb > Setting.captcha_enable? true irb > Setting.captcha_enable = "0" irb > Setting.captcha_enable false irb > Setting.captcha_enable = "1" irb > Setting.captcha_enable true irb > Setting.captcha_enable = "false" irb > Setting.captcha_enable false irb > Setting.captcha_enable = "true" irb > Setting.captcha_enable true irb > Setting.captcha_enable? true irb > Setting.admin_emails ["admin@rubyonrails.org"] irb > Setting.admin_emails = %w[foo@bar.com bar@dar.com] irb > Setting.admin_emails ["foo@bar.com", "bar@dar.com"] irb > Setting.admin_emails = "huacnlee@gmail.com,admin@admin.com\nadmin@rubyonrails.org" irb > Setting.admin_emails ["huacnlee@gmail.com", "admin@admin.com", "admin@rubyonrails.org"] irb > Setting.notification_options { send_all: true, logging: true, sender_email: "foo@bar.com" } irb > Setting.notification_options = { sender_email: "notice@rubyonrails.org" } irb > Setting.notification_options { sender_email: "notice@rubyonrails.org" }version 2.3+
# Get all keys Setting.keys => ["app_name", "host", "default_locale", "readonly_item"] # Get editable keys Setting.editable_keys => ["app_name", "default_locale"] # Get readonly keys Setting.readonly_keys => ["host", "readonly_item"] # Get field Setting.get_field("host") => { scope: :application, key: "host", type: :string, default: "http://example.com", readonly: true } Setting.get_field("app_name") => { scope: :application, key: "app_name", type: :string, default: "Rails Settings", readonly: false } Setting.get_field(:user_limits) => { scope: :limits, key: "user_limits", type: :integer, default: 20, readonly: false } # Get field options Setting.get_field("default_locale")[:options] => { option_values: %w[en zh-CN jp], help_text: "Bla bla ..." }Since: 2.9.0
You can write your custom field type by under RailsSettings::Fields module.
module RailsSettings module Fields class YesNo < ::RailsSettings::Fields::Base def serialize(value) case value when true then "YES" when false then "NO" else raise StandardError, 'invalid value' end end def deserialize(value) case value when "YES" then true when "NO" then false else nil end end end end endNow you can use yes_no type in you setting:
class Setting field :custom_item, type: :yes_no, default: 'YES' endirb> Setting.custom_item = 'YES' irb> Setting.custom_item true irb> Setting.custom_item = 'NO' irb> Setting.custom_item falseversion 2.7.0+
You can use defined_fields method to get all defined fields in Setting.
# Get editable fields and group by scope editable_fields = Setting.defined_fields .select { |field| !field[:readonly] } .group_by { |field| field[:scope] }You can use validates options to special the Rails Validation for fields.
class Setting < RailsSettings::Base # cache_prefix { "v1" } field :app_name, default: "Rails Settings", validates: { presence: true, length: { in: 2..20 } } field :default_locale, default: "zh-CN", validates: { presence: true, inclusion: { in: %w[zh-CN en jp], message: "is not included in [zh-CN, en, jp]" } } endNow validate will work on record save:
irb> Setting.app_name = "" ActiveRecord::RecordInvalid: (Validation failed: App name can't be blank) irb> Setting.app_name = "Rails Settings" "Rails Settings" irb> Setting.default_locale = "zh-TW" ActiveRecord::RecordInvalid: (Validation failed: Default locale is not included in [zh-CN, en, jp]) irb> Setting.default_locale = "en" "en"Validate by save / valid? method:
setting = Setting.find_or_initialize_by(var: :app_name) setting.value = "" setting.valid? # => false setting.errors.full_messages # => ["App name can't be blank", "App name too short (minimum is 2 characters)"] setting = Setting.find_or_initialize_by(var: :default_locale) setting.value = "zh-TW" setting.save # => false setting.errors.full_messages # => ["Default locale is not included in [zh-CN, en, jp]"] setting.value = "en" setting.valid? # => trueIn version 2.3+ you can use Setting before Rails is initialized.
For example config/initializers/devise.rb
Devise.setup do |config| if Setting.omniauth_google_client_id.present? config.omniauth :google_oauth2, Setting.omniauth_google_client_id, Setting.omniauth_google_client_secret end endclass Setting < RailsSettings::Base field :omniauth_google_client_id, default: ENV["OMNIAUTH_GOOGLE_CLIENT_ID"] field :omniauth_google_client_secret, default: ENV["OMNIAUTH_GOOGLE_CLIENT_SECRET"] endYou may also want use Setting before Rails initialize:
config/environments/*.rb If you want do that do that, the setting field must has readonly: true.
For example:
class Setting < RailsSettings::Base field :mailer_provider, default: (ENV["mailer_provider"] || "smtp"), readonly: true field :mailer_options, type: :hash, readonly: true, default: { address: ENV["mailer_options.address"], port: ENV["mailer_options.port"], domain: ENV["mailer_options.domain"], user_name: ENV["mailer_options.user_name"], password: ENV["mailer_options.password"], authentication: ENV["mailer_options.authentication"] || "login", enable_starttls_auto: ENV["mailer_options.enable_starttls_auto"] } endconfig/environments/production.rb
# You must require_relative directly in Rails 6.1+ in config/environments/production.rb require_relative "../../app/models/setting" Rails.application.configure do config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = Setting.mailer_options.deep_symbolize_keys endTIP: You also can follow this file to rewrite ActionMailer's mail method for configuration Mail options from Setting after Rails booted.
https://github.com/ruby-china/homeland/blob/main/app/mailers/application_mailer.rb#L19
Setting.host -> Check Cache -> Exist - Get value of key for cache -> Return | Fetch all key and values from DB -> Write Cache -> Get value of key for cache -> return | Return default value or nil In each Setting keys call, we will load the cache/db and save in ActiveSupport::CurrentAttributes to avoid hit cache/db.
Each key update will expire the cache, so do not add some frequent update key.
Some times you may need to force update cache, now you can use cache_prefix
class Setting < RailsSettings::Base cache_prefix { "you-prefix" } ... endIn testing, you need add Setting.clear_cache for each Test case:
class ActiveSupport::TestCase teardown do Setting.clear_cache end endIf you want to create an admin interface to editing the Settings, you can try methods in following:
config/routes.rb
namespace :admin do resource :settings endapp/controllers/admin/settings_controller.rb
module Admin class SettingsController < ApplicationController def create @errors = ActiveModel::Errors.new setting_params.keys.each do |key| next if setting_params[key].nil? setting = Setting.new(var: key) setting.value = setting_params[key].strip unless setting.valid? @errors.merge!(setting.errors) end end if @errors.any? render :new end setting_params.keys.each do |key| Setting.send("#{key}=", setting_params[key].strip) unless setting_params[key].nil? end redirect_to admin_settings_path, notice: "Setting was successfully updated." end private def setting_params params.require(:setting).permit(:host, :user_limits, :admin_emails, :captcha_enable, :notification_options) end end endapp/views/admin/settings/show.html.erb
<%= form_for(Setting.new, url: admin_settings_path) do |f| %> <% if @errors.any? %> <div class="alert alert-block alert-danger"> <ul> <% @errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %> <div class="form-group"> <label class="control-label">Host</label> <%= f.text_field :host, value: Setting.host, class: "form-control", placeholder: "http://localhost" %> </div> <div class="form-group form-checkbox"> <label> <%= f.check_box :captcha_enable, checked: Setting.captcha_enable? %> Enable/Disable Captcha </label> </div> <div class="form-group"> <label class="control-label">Admin Emails</label> <%= f.text_area :admin_emails, value: Setting.admin_emails.join("\n"), class: "form-control" %> </div> <div class="form-group"> <label class="control-label">Notification options</label> <%= f.text_area :notification_options, value: YAML.dump(Setting.notification_options), class: "form-control", style: "height: 180px;" %> <div class="form-text"> Use YAML format to config the SMTP_html </div> </div> <div> <%= f.submit 'Update Settings' %> </div> <% end %>You can use cache_store to change cache storage, default is Rails.cache.
Add config/initializers/rails_settings.rb
RailsSettings.configure do self.cache_storage = ActiveSupport::Cache::RedisCacheStore.new(url: "redis://localhost:6379") end🚨 BREAK CHANGES WARNING: rails-settings-cached 2.x has redesigned the API, the new version will compatible with the stored setting values by an older version. When you want to upgrade 2.x, you must read the README again, and follow guides to change your Setting model. 0.x stable branch: https://github.com/huacnlee/rails-settings-cached/tree/0.x
For new project / new user of rails-settings-cached. The ActiveRecord::AttributeMethods::Serialization is best choice.
This is reason of why rails-settings-cached 2.x removed Scoped Settings feature.
For example:
We wants a preferences setting for user.
class User < ActiveRecord::Base serialize :preferences end @user = User.new @user.preferences[:receive_emails] = true @user.preferences[:public_email] = true @user.save- ruby-china/homeland - master
- forem/forem - 2.x
- siwapp/siwapp - 2.x
- aidewoode/black_candy - 2.x
- huacnlee/bluedoc - 2.x
- getzealot/zealot - 2.x
- kaishuu0123/rebacklogs - 2.x
- texterify/texterify - 2.x
- mastodon/mastodon - 0.6.x
- helpyio/helpy - 0.5.x
- maybe-finance/maybe - 2.x
And more than 1K repositories used.