Rails Active Admin: from config File to Admin Portal


This blog post is a continuation of part 1 that had focused on the the installation process of Active Admin. In the current post, we show how Active Admin can be used to migrate from a configuration file based approach by an admin portal based approach. We start with a configuration file a la figaro, and migrate a boolean-like variable to Active Admin Portal, allowing admins to manage this variable. We will follow a DRY (don’t repeat yourself) approach by help of a “catch all” method that can handle any future variable of the same kind without the need to change any Ruby code.

Versions

  1. first published version
  2. changed model name ‘Config’ by ‘SystemSetting’, since RbConfig used by travisCI has a problem with the ‘Config’ name. Note, that the remaining error of v1 has disappeared with this measure as well: it seems that also Active Admin itself had a problem with the name ‘Config’.

How to use Active Admin to define System Settings

Now let us use go to work and prepare the Active Admin console for allowing to manage system wide variables:

Prerequisites

  • We assume that Active Admin is already installed. See part 1 for step by step installation instructions.
  • Rails server is started, e.g. with the command rails s

Step 1: create a model

Let us generate a model that will hold the configuration variables I am managing via figaro today. I want to support Strings and Booleans in a single class, so I have added a value_type column:

$ rails g model SystemSetting name:string value_type:string value_default:string value:string short_description:string description:text
      invoke  active_record
      create    db/migrate/20160205102214_create_system_settings.rb
      create    app/models/system_setting.rb
      invoke    rspec
      create      spec/models/system_setting_spec.rb

We need to migrate the database changes:

rake db:migrate

If you have forgotten to add a column, you can add it later by issuing commands similar to:

rails g migration AddNewColumnToSystemSetting new_column:text
rake db:migrate

Step 2: register the model with Active Admin:

Now let us add this model to Active Admin:

$ rails generate active_admin:resource SystemSetting
      create  app/admin/system_setting.rb

Step 3: log into the Active Admin console and add a SystemSetting entry:

After refreshing the Active Admin console, a new menu item SystemSettings shows up in the Active Admin page (log in; default user:pass = admin@example.com:password). When we follow the link, we see:

2016-02-05_113246_capture_018

No let us click on the Create one link in the SystemSettings page above and we can enter our first variable:

2016-02-05_113619_capture_019

Oups?

2016.02.03-21_28_39-hc_001

Okay, we have not yet specified in a controller, which attributes are allowed. In a normal model, this would be done in the controller by a command like

def user_params
 params.require(:user).permit(:username, :email, :password, :salt, :encrypted_password)
end

For Active Admin, it is similar, but different: it is described in the Active Admin documentation (found via this stackoverflow answer):

In app/admin/system_setting.rb add the permit_params line:

ActiveAdmin.register SystemSetting do
   permit_params :name, :value_type, :value_default, :value, :short_description, :description ## add this line

Now, press the browser’s back button and retry to save the entry. We will get:

2016-02-05_114100_capture_020

Step 4: use the new variable in the project source code

Let us make use of the new variable:

The following code snippet shows, how I use the system variable WEBPORTAL_SIMULATION_MODE today:

if ENV[WEBPORTAL_SIMULATION_MODE] == "true"
...
end

I would like to replace that snippet by something like:

if SystemSetting.webportal_simulation_mode
...
end

This is simpler than the more obvious standard handling of database searches like follows:

values =SystemSetting.where(name: "WEBPORTAL_SIMULATION_MODE")
value = values[0] if values.count == 1 
if value == "true" 
... 
end

In addition, the SystemSetting.webportal_simulation_mode approach will allow for more functionality as we will see below. The value of the SystemSetting.webportal_simulation_mode method shall be read from the SystemSetting database entry with name=”WEBPORTAL_SIMULATION_MODE”.

Step 4.1: create a catch all method in the model

Since we do not want to create such a method for each and every variable, I have made use of a cool Ruby feature: the method_missing method, which is a kind of catch all method. Any method that is not defined upfront will be handled by the method_missing method.

The code I have created looks like follows:

class SystemSetting < ActiveRecord::Base
    
    # catch any method like SystemSetting.whatever and do something with it instead of raising an error
    # found on http://stackoverflow.com/questions/185947/ruby-define-method-vs-def?rq=1
    def self.method_missing(*args)
        # assumption:
        # if we call SystemSetting.webportal_simulation_mode, we assume that the associated environment variable reads WEBPORTAL_SIMULATION_MODE (all capitals)
        environment_variable = args[0].to_s.upcase
        
        # look for database entries matching the method name, but with all capitals:
        foundlist =  self.where(name: environment_variable)
        
        # return value, if non-ambiguous entry was found; else return environment variable, if it exists:
        if foundlist.count == 1
            # found in the database: return its value as boolean
            foundlist[0].value == "true"
        elsif foundlist.count == 0 && !ENV[environment_variable].nil?
            # not found in the database: try to find corresponding environment variable as a fallback. 
            value = ENV[environment_variable]
            
            # If found, auto-create a database entry
            self.new(name: environment_variable, value: value, value_type: :boolean).save!
            
            # return its value as boolean
            value == "true"
        elsif foundlist.count > 1
            # error handling: variable found more than once (should never happen with the right validation)
            abort "Oups, this looks like a bug: Configuration.variable with name #{environment_variable} found more than once in the database."
        else
            # error handling: variable not found:
            message = "#{environment_variable} not found: neither in the database nor as system environment variable." +
                      " As administrator, please create a SystemSetting variable with name #{environment_variable} " +
                      " and the proper value (in most situations: 'true' or 'false') on the Active Admin Console on https://localhost:3000/admin/system_settings " +
                      "(please adapt the host and port to your environment). Alternatively, restart the server. " +
                      "This should reset the environment variable to its default value and the SystemSetting variable will be auto-created."
            abort message
        end # if foundlist.count == 1
    end # def self.method_missing(*args)
    
end

The method_missing catch all trick works like follows:

  1. the method_missing catch all method will handle any occurrence of e.g. SystemSetting.webportal_simulation_mode used anywhere in the project.
  2. The first argument handed over to method_missing (i.e. arg[0]) will be a symbol like :webportal_simulation_mode.
  3. We convert this argument to “WEBPORTAL_SIMULATION_MODE” and search for this name pattern in the SystemSetting database.
  4. If there is one and only one matching entry in the database, we return its value as a boolean. If there is no matching entry in the database, but the environment variable WEBPORTAL_SIMULATION_MODE is set, we auto-generate a corresponding entry in the database.

Now we can replace all occurrences of ENV[WEBPORTAL_SIMULATION_MODE] == "true" within the project by SystemSetting.webportal_simulation_mode.

Step 4.2: correct the destroy behavior

If we now want to destroy a SystemSetting that is being used in the project code, it will be auto-created, when SystemSetting.webportal_simulation_mode is called. This might not be the desired behavior. If I delete a SystemSetting, I do not want it to pop up again. Let us fix that now:

 

class SystemSetting < ActiveRecord::Base
    def self.method_missing(*args)
    ...
        # If found, auto-create a database entry
        @@autocreate = {} unless defined?(@@autocreate)
        @@autocreate[environment_variable] = true if @@autocreate[environment_variable].nil?
        self.new(name: environment_variable, value: value, value_type: :boolean).save! if @@autocreate[environment_variable]
    ...  
    def destroy
        # if we destroy a database entry, we do not want it to be auto-created again:
        @@autocreate = {} unless defined?(@@autocreate)
        @@autocreate[name] = false
        
        super
    end
    ...
end

Note, that I had defined a fallback, i.e. the SystemSetting.webportal_simulation_mode will still return a meaningful value, if the environment variable WEBPORTAL_SIMULATION_MODE is defined via configuration file (not visible to the Active Admin portal user). Therefore, the code  SystemSetting.webportal_simulation_mode within the project will not raise an exception in this case.

Step 4.3 add validations

A SystemSetting myvar will be mapped to an environment variable MYVAR and another SystemSetting MyVar will be mapped to the same environment variable MYVAR. Therefore we need to make sure we do not allow for two SystemSettings with the same meaning. We will allow for SystemSetting names with all capital letters only. For that we add following validations:

class SystemSetting < ActiveRecord::Base
    ...  
    # prevent that a name can exist twice:
    validates :name, uniqueness: true
 
    # allow only variables with capital letters and underscores:
    validates_format_of :name, :with => /\A[A-Z0-9_\.]{1,255}\Z/, message: "needs to consist of 1 to 255 characters: A-Z, 0-9, . and/or _"
end

We have achieved our goal: our first configuration variable WEBPORTAL_SIMULATION_MODE has been moved from a configuration file to the database and can be controlled by the administrator via the Active Admin console.

thumps_up_3

Summary

We have migrated a boolean configuration setting from a configuration file to the database in order to allow the manipulation of its setting from Active Admin console. We have encountered a Active Admin problem with the view of a single entry, but this did not prevent us to reach our goal (troubleshooting and/or error report to active Admin is postponed…).

Instead of requiring a standard database reading syntax like

values = SystemSetting.where(name: "WEBPORTAL_SIMULATION_MODE")
value = values[0] if values.count == 1
if value == "true"
...
end

we allow for a simpler syntax like

if SystemSetting.webportal_simulation_mode
...
end

In order to prepare the solution for more upcoming variables of the same kind, we use the  method_missing method as a catch all method. With this approach, any new variable like

SystemSetting.whatever

can be handled with the same piece of code. An administrator can add new variables in the Active Admin portal without the need to write a single line of ruby code. Okay, the SystemSetting.whatever must be used somewhere in the project’s ruby code to have a function.😉

For convenience and backwards compatibility with the configuration file, I had added an auto-create function (the line starting with self.new in the  method_missing method): for any already defined environment variable, a database entry is created automatically from the environment variable. This will help for a quick migration of all variables from the configuration file to the database. Moreover, it allows us to keep the configuration file as a fallback, e.g. if the database is destroyed. However, in future, it will be better to automatically export the system_setting table from the database and re-import it upon server start if the system_setting table is found empty (or allow to retrieve it from some SW repository).

The results of the measure can be reviewed in the open source on Github, e.g. on this specific commit reflecting the work status at the end of the blog post.

Next steps: TODO: allow for non-boolean-like variables

Idea:

  • re-write the code that methods like SystemSetting.what_is_the_weather_today? with a question mark are handled as booleans.
  • handle methods without question mark (e.g. SystemSetting.weather_report) as strings.
  • convert such string values to a numbers, it the string values are convertible.

One thought on “Rails Active Admin: from config File to Admin Portal

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s