Rails Best Practices 2: Move logic to model

Today we continue our analysis of the Rails Best Practices.
In the previous post we saw Named Scope, Model Association and
Following the same direction, in today’s post we’ll examine the use of Callback Model and Virtual Attribute.

1. Virtual Attribute

Suppose we have a customers list table defined as follows

1
2
3
4
5
6

create_table "clients", :force => true do |t|
t.string "first_name"
t.string "last_name"
t.string "street"
t.string "city"
end

Suppose we have a customers list table defined as follows but we want to define an input mask where street and city are grouped into one field called “address”.
The form with the “address” field will be defined as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

<h1>Create Client</h1>
<% form_for @client do |f| %>
<ol class="formList">
<li>
<%= f.label :first_name, 'First Name' %>
<%= f.text_field :first_name %>
</li>
<li>
<%= f.label :last_name, 'Last Name' %>
<%= f.text_field :last_name %>
</li>
<li>
<%= f.label :address, 'Address' %>
<%= text_field_tag :address %>
</li>
</ol>
<% end %>

Now in the create method we’ll have something like:

1
2
3
4
5
6
7
8
9
10
11
12

class ClientsController < ApplicationController

def create
@client = Client.new(params[:client])
@client.street = params[:address].split(' ', 2).first
@client.city = params[:address].split(' ', 2).last
@client.save
end

...

end

We can now improve this method by defining the address field as a virtual attribute of the Client model.

1
2
3
4
5
6
7
8
9
10
11
12

class Client < ActiveRecord::Base

def address
[street, city].join(' ')
end

def address=(addr)
split = addr.split(' ', 2)
self.street = split.first
self.city = split.last
end
end

In the form we can now define the address field as f.field_tag instead of as text_field_tag :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

<h1>Create Client</h1>
<% form_for @client do |f| %>
<ol class="formList">
<li>
<%= f.label :first_name, 'First Name' %>
<%= f.text_field :first_name %>
</li>
<li>
<%= f.label :last_name, 'Last Name' %>
<%= f.text_field :last_name %>
</li>
<li>
<%= f.label :address, 'Address' %>
<%= f.text_field :address %>
</li>
</ol>
<% end %>

Finally, the controller will become simpler:

1
2
3
4
5
6
7
8
9
10

class ClientsController < ApplicationController

def create
@client = Client.new(params[:client])
@client.save
end

...

end

2. Model Callback
Now we see a method, called Model Callback that allows us, as with the virtual attribute just seen, to simplify the form and to move the controller’s logic inside the model.

Suppose we have to implement a feature that automatically associates a set of tags to a post.
This feature, called calculate_tags return a list of tags based on the most frequent words contained in the post.
We don’t see the code of this function and instead see how and when to invoke the automatic generation of tags.

A first implementation can be as follows. Given the following form:

1
2
3
4

<% form_for @post do |f| %>
<%= f.text_field :content %>
<%= check_box_tag 'calculate_tags' %>
<% end %>

the corresponding create method will be:

1
2
3
4
5
6
7
8
9
10

class PostController < ApplicationController
def create
@post = Post.new(params[:post])
if params[:calculate_tags] == '1'
@post.tags = TagGenerator.generate(@post.content)
else
@post.tags = ""
end
@post.save
end

Editing the Post model, let’s see how to improve the code we’ve just wrote.
First we introduce in our model an attribute, “calculate_tags”, and a filter “generate_tags”, of type before_save

1
2
3
4
5
6
7
8
9
10

class Post < ActiveRecord::Base
attr_accessor :calculate_tags
before_save :generate_tags

private
def generate_tags
return unless calculate_tags == '1'
self.tags = TagGenerator.generate(self.content)
end
end

Returning to the form we can now redefine the field calculate_tags as f.check_box

1
2
3
4

<% form_for @post do |f| %>
<%= f.text_field :content %>
<%= f.check_box 'calculate_tags' %>
<% end %>

Finally, our create method in the controller will return to its simplest form.

1
2
3
4
5
6

class PostController < ApplicationController
def create
@post = Post.new(params[:post])
@post.save
end
end