Rails: How to use form-models to make statistic queries
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.