Rails: How to use form-models to make statistic queries

4 minute read

Out Of Date Warning

This article was published on 23/08/2014, this means the content may be out of date or no longer relevant.
You should verify that the technical information in this article is still up to date before relying upon it for your own purposes.

Query interfaces for users or administrators are a common usecase of web-apps. Wether widget configurators, or to filter various statistics over time. Rails doesn't suggest a default way for handling those cases, but provides with ActiveModel::Model a very good interface to start implementing it. This works great for versions of Rails >= 4, with more boilerplate also for 3.1, 3.2.

As example, we will implement a statistic form for a bill model, to quickly see, how many bills where issued, to what kind of customer etc.

Table of Contents

Routes and service models

Usually, I start out with implementing the controller and routes. When you plan on implementing a lot of those little query forms, it is a good idea, to put them in a namespace=folder.

rails g controller statistics bills

Next, we need to implement the active model. Some people put those in app/models, too. I prefer giving those a new folder, like app/services or app/forms. You can just create that folder, Rails will auto load everything as needed from there (Note: you have to restart the server/console once, as Rails scans those folder on boot time).

# app/services/bill_statistics.rb
class BillStatistics
  include ActiveModel::Model
  attr_accessor :from
  attr_accessor :to
  attr_accessor :bill_type
end

Controller + View

We are using simple form for quickly generating that form. Formtastic should work too.

# app/controllers/statistics_controller.rb

class StatisticsController
  def bills
    @bill = BillStatistics.new(bill_statistic_params)
  end

  # Rails 4: Strong params
  private
  def bill_statistic_params
    params.require(:bill_statistics).permit!
  end
end

We initialize our form model with any parameters. If the params are empty, they will be empty. If you are using Rails 4/4.1 you need to consider strong params instead.



<%= simple_form_for @bill, url: '', method: 'get' do |f| %>
  <%= f.input :from, as: :string %>
  <%= f.input :to, as: :string %>
  <%= f.input :bill_type, as: :radio_buttons, collection: [:membership, :demand, :consulting] %>
  <%= f.submit 'query' %>
<% end %>


<%= @bill.statistics %>

Using a form library like simple form or Formtastic makes it easy to generate that form. If you are using Bootstrap and configured the styles of the form to match those, I can't think of a faster way to quickly produce this kind of form. Leaving the url empty and using the method get will send the content of the form to the same action. If your filters and inputs increases, consider switching to post.

Enhancements with attributes and validations

Up to now, we have no validations or parameter conversions in place.

Validations

Just include ActiveModel::Validations in our model. This will give you access to the default validations, the valid?, and errors methods.

# app/services/bill_statistics.rb
class BillStatistics
  include ActiveModel::Model
  include ActiveModel::Validations
  attr_accessor :from
  attr_accessor :to
  attr_accessor :bill_type

  validates :from, presence: true
  validates :to, presence: true

end
# app/controllers/statistics_controller.rb

  def bills
    @bill = BillStatistics.new(bill_statistic_params)
    if params[:bill_statistics]
      @bill.valid?
    end
  end

We just trigger the validations if the user supplied any parameters. This will fill the errors attribute of the query model and SimpleForm will display them.

Parameter conversions

Unfortunately, I do not know of a easy way to do this without using another Gem. Virtus is a very good one, which works with and without ActiveModel without problems:

# Gemfile
gem 'virtus'
# app/services/bill_statistics.rb
class BillStatistics
  include ActiveModel::Model
  include ActiveModel::Validations
  include Virtus.model
  attribute :from, Date, default: ->{ 2.years.ago }
  attribute :to, Date, default: -> { Date.today }
  attribute :bill_type, String

  validates :from, presence: true
  validates :to, presence: true

end

Going from here: Building up statistics and charts

From here, most of my form models differ, as every statistic page is a little bit different. Common patterns are:

  • Defining a query function for the model
  • Displaying aggregated informations in a table
  • Displaying a line chart for time values (Chartkick or custom Highcharts)
class BillStatistics
  ...
  def bills
    scope = Bill.all
    scope = scope.where('created_at >= ?', from)
    scope = scope.where('created_at <= ?', to)
    if bill_type
      scope = scope.where(bill_type: bill_type)
    end
    scope
  end

  def statistics
    {
      count: bills.count,
      sum: bills.sum(:total),
    }
  end
end

I usually start out with a query builder method similar to above, which conditionally builds up the search query. The statistics can be used in the view and just rendered as a table.

Of course, now our BillStatistics class has 2 responsibilities: Validating/Providing the form and calculating the statistics. If the statistic methods grow in complexity and numbers, I tend to create own classes for those.

Conclusion

Using something like SimpleForm, you get a lot of things for free:

  • Forms redisplay their old values without hassle
  • Validations works almost the same as in database models (except association validations)
  • I18n via simple-form to set labels and hints via configuration instead of cluttering the view.
  • With virtus: Default values and conversions.

TL;DR: Using form models is a convenient and Railsy way to handle and validate complex query forms efficiently.