Models in a Rails application can easily become very complicated as more logic gets added to them. Fortunately there are several ways to refactor them that can help to clean up them up. Most of the behaviour in the User
model of our example application is to do with forms. It has some custom virtual accessors, a number of validations, some callbacks and an association that accepts nested attributes for another model. It also has some virtual attributes and custom validations.
class User < ActiveRecord::Base
has_secure_password
attr_accessor :changing_password, :original_password, :new_password
validates_presence_of :username
validates_uniqueness_of :username
validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/
validates_length_of :password, minimum: 6, on: :create
validate :verify_original_password, if: :changing_password
validates_presence_of :original_password, :new_password, if: :changing_password
validates_confirmation_of :new_password, if: :changing_password
validates_length_of :new_password, minimum: 6, if: :changing_password
before_create :generate_token
before_update :change_password, if: :changing_password
has_one :profile
accepts_nested_attributes_for :profile
def subscribed
subscribed_at
end
def subscribed=(checkbox)
subscribed_at = Time.zone.now if checkbox == "1"
end
# Other methods omitted.
end
By extracting these out into form objects we can clean up our model considerably.
# app/forms/password_form.rb
class PasswordForm
include ActiveModel::Model
def persisted?
false
end
attr_accessor :original_password, :new_password
validate :verify_original_password
validates_presence_of :original_password, :new_password
validates_confirmation_of :new_password
validates_length_of :new_password, minimum: 6
def initialize(user)
@user = user
end
def submit(params)
self.original_password = params[:original_password]
self.new_password = params[:new_password]
self.new_password_confirmation = params[:new_password_confirmation]
if valid?
@user.password = new_password
@user.save!
true
else
false
end
end
def verify_original_password
unless @user.authenticate(original_password)
errors.add :original_password, "is not correct"
end
end
end
# app/forms/signup_form.rb
class SignupForm
include ActiveModel::Model
def persisted?
false
end
def self.model_name
ActiveModel::Name.new(self, nil, "User")
end
validates_presence_of :username
validate :verify_unique_username
validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/
validates_length_of :password, minimum: 6
delegate :username, :email, :password, :password_confirmation, to: :user
delegate :twitter_name, :github_name, :bio, to: :profile
def user
@user ||= User.new
end
def profile
@profile ||= user.build_profile
end
def submit(params)
user.attributes = params.slice(:username, :email, :password, :password_confirmation)
profile.attributes = params.slice(:twitter_name, :github_name, :bio)
self.subscribed = params[:subscribed]
if valid?
generate_token
user.save!
profile.save!
true
else
false
end
end
def subscribed
user.subscribed_at
end
def subscribed=(checkbox)
user.subscribed_at = Time.zone.now if checkbox == "1"
end
def generate_token
begin
user.token = SecureRandom.hex
end while User.exists?(token: user.token)
end
def verify_unique_username
if User.exists? username: username
errors.add :username, "has already been taken"
end
end
end
Now methods :new and :create in our UsersController
will look much cleaner.
def new
@signup_form = SignupForm.new
end
def create
@signup_form = SignupForm.new
if @signup_form.submit(params[:user])
session[:user_id] = @signup_form.user.id
redirect_to @signup_form.user, notice: "Thank you for signing up!"
else
render "new"
end
end
Here is an example of how we are gonna use it.
# users/new.html.erb
<%= form_for @signup_form do |f| %>
The same works for PasswordsController
.
def new
@password_form = PasswordForm.new(current_user)
end
def create
@password_form = PasswordForm.new(current_user)
if @password_form.submit(params[:password_form])
redirect_to current_user, notice: "Successfully changed password."
else
render "new"
end
end
Usage:
# passwords/new.html.erb
<%= form_for @password_form, url: passwords_path, method: :post do |f| %>
Posted by Alexander M to Ruby and RoR knowledge base (2016-09-05 15:00)