I've been using rails for a while, and my controller code starts to get out of control (no pun intended.) I've heard you want skinny controllers and fat models, and for me this hasn't come naturally in my rails progression.
I'll post one model of my rails app.
line_item #create
def create
#cart = current_cart
#product is built from base_product, after finding associated product
#base_product_id = params[:line_item][:base_product_id]
get_product_from_base_product
#line_item = #cart.line_items.build(
:product_id => #product_id,
:order_id => nil,
:weight => params[:line_item][:weight],
:quantity => params[:line_item][:quantity]
)
## Does a line item with the same product_id already exist in cart?
if #line_item.exists_in_collect?(current_cart.line_items)
#if so, change quantity, check if there's enough stock
if current_cart.where_line_item_with(#product_id).update_quantity(#line_item.quantity) == true
#line_item.destroy
redirect_to base_products_path
flash[:success] = "Detected Producted In Cart, Added #{#line_item.quantity} More to Quantity"
else
redirect_to cart_path
flash[:failure] = "Cannot Add To Cart, We Only Have #{#line_item.product.stock_qty} In Stock"
end
else
if #line_item.stock_check == true
if #line_item.save
respond_to do |format|
format.html { redirect_to base_products_path,
:notice => "(#{#line_item.quantity}) #{#line_item.product.base_product.title} Added to Cart." }
format.xml { render :xml => #line_item,
:status => :created, :location => #line_item }
end
else
format.xml { render :xml => #line_item.errors,
:status => :unprocessable_entity }
end
else
redirect_to base_products_path
if #line_item.product.stock_qty > 0
flash[:failure] = "Sorry! We Only Have #{#line_item.product.stock_qty} In Stock"
else
flash[:failure] = "Sorry! That Item Is Out Stock"
end
end
end
end
controller methods:
private
def get_product_from_base_product
#product_id = Product.where(:base_product_id => #base_product_id).where(:size_id => params[:line_item][:size]).first.id
end
LineItem model
class LineItem < ActiveRecord::Base
belongs_to :product
belongs_to :cart
after_create :set_order_weight#, :set_package_dimentions
after_update :set_order_weight#, :set_package_dimentions
#max capactiy here
def have_enough_product?
if self.product.stock_qty > self.quantity
return true
else
self.quantity = self.quantity_was
self.save
return false
end
end
def over_order_cap?
if self.quantity > 50
return true
end
end
def update_quantity(qty)
if self.have_enough_product? == true
self.quantity += qty
self.save
end
end
def stock_check
if self.product.stock_qty > self.quantity == true
return true
else
return false
end
end
def exists_in_collect?(items)
items.each do |item|
return true if self.product_id == item.product_id
end
return false
end
As you can see, this is somewhat large controller. Is this normal? All the logic is happening there. A user may already have this item in the cart, this item may not be in stock, this item has a capacity and the user is trying to order more than that. What's more, I
I know this isn't a typical stack question. Nothing is broken, but I just don't feel good about my code.
I don't want to make a 'homework' question, but I'd appreciate suggestions on how to refactor controller code with models. I'd appreciate references and suggestions.
Thanks!
update added cart.rb
class Cart < ActiveRecord::Base
has_many :line_items#, dependent: :destroy
has_one :order
def where_line_item_with(prod_id)
line_items.where(:product_id => prod_id).first
end
def sub_total
self.line_items.to_a.sum { |item| item.total_price }
end
end
I'll answer not because I'm an expert, but because I just recently went through this myself. For me, the realization came when I began putting my app under a test suite. I was having a really tough time testing my controllers actions because they did so many different things.
Here are a couple of reasons why I think you should move and separate the logic of your controller into model methods:
Reuse of code. I found that instead of writing the same code 3 or 4 different times in different controllers, I could write it once (and test it once!) in the model and call those methods in my controllers.
Testing. Rather than testing the same logic in 3 or 4 controller actions, you can test it once in the model spec (easier to test there as well). Not only does it save you time writing tests, but it removes the possibility that you implement things differently across controllers for what should be the same logic.
Readability. I'll be honest, I didn't read through your entire create action because it wasn't very readable. I had a couple similar actions, and I hated trying to debug them because it was hard to follow what was happening. Moving that logic to the model (and separating it into smaller, more specific methods) allows you to focus on the things a controller should be doing (auth, directing, handling params etc.).
So, how to refactor it? I read something in a SO question (can't find it now) that essentially said: if the logic deals with the way resources are associated, it should be in the model. I think that's a pretty good general rule. I would start there. If you don't have a test suite, I would add that simultaneously.
Related
I'm creating a website that let people read short stories in several chapters.
For this, I nested a chapter scaffold inside a novel scaffold, and linked (novel has_many :chapters, chapters belongs_to :novel) them both together.
However I'm trying to get the chapter's number inside the URL (instead of the id, which never decreases). Setting the chapter as is is not a problem, but I'd like to automate the chapter number without my users needing to add it themselves.
To do so, I figured that all I needed was to get how many chapter the current novel had by checking self.class.where(:novel_id => #novel).count. At this point, I have no issues, however it gets complicated when I try to increment this number, I get the error: undefined method 'anoter_one' for 0:Fixnum
Here is my "another_one" function inside my model (I tried some things)
def another_one
#number = self.class.where(:novel => #novel).count.to_i
#number.increment
end
Here is the controller
def create
#novel = Novel.find(params[:novel_id])
#chapter = Chapter.new(chapter_params)
#chapter.chapter_number.another_one
#chapter.novel = #novel
if #chapter.save
redirect_to novel_chapter_path(#novel, #chapter), notice: 'Chapter was successfully created.'
else
render :new
end
end
What am I doing wrong ?
Thank you in advance
Your calling anoter_one - which is a misspelling of another on the value of #chapter.chapter_number - not the model.
One way to solve this is by using an association callback:
class Novel
has_many :chapters, before_add: :set_chapter_number
def set_chapter_number(chapter)
if chapter.chapter_number.blank?
chapter.chapter_number = self.chapters.size + 1
end
end
end
In order for the callback to be called properly you want to build the associated items off the parent:
def new
#novel = Novel.find(params[:novel_id])
#chapter = #novel.chapters.new
end
def create
#novel = Novel.find(params[:novel_id])
#chapter = #novel.chapters.new(chapter_params)
if #chapter.save
redirect_to [#novel, #chapter], notice: 'Chapter was successfully created.'
else
render :new
end
end
I'm in Rails 3. Here's my code for a method which creates Update records in response to certain attributes being changed on a model called Candidate:
before_save :check_changed, on: [:update]
def check_changed
tracked_attributes = ["period_contributions", "total_contributions",
"period_expenditures", "total_expenditures",
"debts_and_loans", "cash_on_hand",
"major_endorsements",
"rating_afl_cio",
"rating_cal_tax",
"rating_cc",
"rating_eqca",
"rating_lcv",
"rating_now"]
changes.each do |key, value|
if tracked_attributes.include?(key)
Update.create(:candidate_id => self.id, :attribute_name => key,
:new_value => value[1], :old_value => value[0])
end
end
end
The issue is that I have some rake tasks I'm running to do batch updates to the data, which end up triggering this callback unintentionally. I'd like for it only to run when a Candidate is updated from within the admin tool aka CRUD interface. Any advice on the best way to do this?
I will only use callbacks when it is something that always needs to happen, no matter the source. Magically skipping or including them normally leads to pain down the road.
My suggestion is to create a different method on the model that does the check and use that for the crud actions.
class Candidate
#...
def check_changed_and_update(attributes)
check_changed
update(attributes)
end
private
def check_changed
tracked_attributes = ["period_contributions", "total_contributions",
"period_expenditures", "total_expenditures",
"debts_and_loans", "cash_on_hand",
"major_endorsements",
"rating_afl_cio",
"rating_cal_tax",
"rating_cc",
"rating_eqca",
"rating_lcv",
"rating_now"]
changes.each do |key, value|
if tracked_attributes.include?(key)
Update.create(:candidate_id => self.id, :attribute_name => key,
:new_value => value[1], :old_value => value[0])
end
end
end
end
Then in the controller for candidate just change update to check_changed_and_update:
class CanidateController < ApplicationController
def update
#...
respond_to do |format|
if #candidate.check_changed_and_update(canidate_params)
format.html { redirect_to #candidate, notice: 'Candidate was successfully updated.' }
else
format.html { render action: 'edit' }
end
end
end
This has an added bonus of making it obvious what is going to happen when update is called.
Now you can just use the normal active record api in your rake tasks.
What I'm thinking right now is...
I have a library full of books (entries). Each book has many checkouts (embedded document).
What I think I want to do is, upon checkout, make a new "checkout" as an embedded document. Upon checkin, I want to edit the checkout and add a "date_checked_out" field...
The issue is, my current model/controller makes a new entry each time there is a checkin or checkout...so it's doubly redundant...
What's the best way to go about this? Need more detail?
Checkout Controller:
def new
#entry = Entry.find(params[:entry_id])
#checkout = #entry.checkout.new
respond_to do |format|
format.html {render :layout => false}
end
end
def create
#entry = Entry.find(params[:entry_id])
#entry.update_attributes(:checked_out => "Out")
#checkout = #entry.checkout.create!(params[:checkout])
redirect_to "/", :notice => "Book Checked Out!"
end
class Checkout
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::MultiParameterAttributes
field :checkout_date, :type => Time
field :checkout_date_due, :type => Time
field :book_in, :type => Time, :default => Time.now
field :book_out, :type => Time, :default => Time.now
embedded_in :entries, :inverse_of => :entries
end
It makes sense the checkout would have a start and stop date. Do you need to make a checkout when a checkin occurs? You may be able to change this to an 'update' instead of a 'create' on the checkout controller - enter a checked_in_at on update.
Specifically - you'd want to be able to accept a PUT on the checkout controller - this could either be generic (allowing you to update the checkout in many ways) or specific, make a route that cleans up this for you:
resources :checkouts do
put :checkin, :on => :member
end
in checkouts_controller
def checkin
#checkout = Checkout.find(params[:id]
#checkout.update_attribute(:checked_in_at, Time.now)
# handle issues, redirect, etc.
end
Keeping it pure REST, add an update action to your Checkout controller.
Also, post your entry model. I'm assuming from your code that an entry has_one checkout, and a checkout belongs to an entry.
Something like:
*Edit because it appears OP wants to see how this works while checking for a conditional
... original boilerplate code ommitted
def update
#entry = Entry.find(params[:entry_id])
# if the book is checked out
if #entry.checked_out == "out"
# then update it
#entry.update_attributes(:checked_out => "whatever" # though I'd seriously consider changing checked out to a boolean - if it's either out or in, true or false makes sense. Ignore this advice if there are more than two states
#checkout = #entry.checkout
respond_to do |format|
if #checkout.update_attributes(:checked_out => "newValue")
...
else
.. handle errors
end
end
else
#the book does not have the correct status
respond_to do |format|
format.html { redirect_to some_action_path, :notice => "Entry is not out, so we cannot update its status." }
format.json { render json: #entry.errors, status: :unprocessible_entry }
end
end
end
Also, if you want to make the code a bit more explicit, you might consider taking swards advice and creating a few named endpoints like
def checkout
end
def checkin
end
I think that makes sense, in that someone else reading the code can very easily know exactly what that controller action is doing, as opposed to create and update.
Say I open a Rails (2.3.8) script console and try this:
a = Account.new(:first_name) = 'foo'
i = a.invoices.build
p i.account.first_name
Account.rb is a model object and contains:
has_many :invoices
and Invoice.rb is a model as well containing:
belongs_to :account, :validate => true
In console line 3 above, i.account is nil. I realize that i.account would not be nil if account had been saved, but I do not wish to save an account unless I can create a valid invoice for the account. And, just for kicks, the invoice validation depends on some properties of the unsaved account.
Any ideas how to make this work?
Best,
Will
I typically do this with transactions. With rails transactions you can perform db interactions and roll them back at any time if something fails to validate. For example:
in your model:
def save_and_create_invoice
Account.transaction do
#first let's save the account, this will give us an account_id to work with
return false unless self.save
invoice = self.invoices.build
#setup your invoice here and then save it
if invoice.save
#nothing wrong? return true so we know it was ok
return true
else
#add the errors so we know what happened
invoice.errors.full_messages.each{|err| errors.add_to_base(err)}
#rollback the db transaction so the account isn't saved
raise ActiveRecord::Rollback
#return false so we know it failed
return false
end
end
end
And in your controller you would call it like so:
def create
#account = Account.new(params[:account])
respond_to do |format|
if #account.save_and_create_invoice
format.html
else
format.html {render :action => "new"}
end
end
end
Note that I didn't run this code to test it, just whipped it out real quick to show an example.
We recently began a compliance push at our company and are required to keep a full history of changes to our data which is currently managed in a Rails application. We've been given the OK to simply push something descriptive for every action to a log file, which is a fairly unobtrusive way to go.
My inclination is to do something like this in ApplicationController:
around_filter :set_logger_username
def set_logger_username
Thread.current["username"] = current_user.login || "guest"
yield
Thread.current["username"] = nil
end
Then create an observer that looks something like this:
class AuditObserver < ActiveRecord::Observer
observe ... #all models that need to be observed
def after_create(auditable)
AUDIT_LOG.info "[#{username}][ADD][#{auditable.class.name}][#{auditable.id}]:#{auditable.inspect}"
end
def before_update(auditable)
AUDIT_LOG.info "[#{username}][MOD][#{auditable.class.name}][#{auditable.id}]:#{auditable.changed.inspect}"
end
def before_destroy(auditable)
AUDIT_LOG.info "[#{username}][DEL][#{auditable.class.name}][#{auditable.id}]:#{auditable.inspect}"
end
def username
(Thread.current['username'] || "UNKNOWN").ljust(30)
end
end
and in general this works great, but it fails when using the "magic" <association>_ids method that is tacked to has_many :through => associations.
For instance:
# model
class MyModel
has_many :runway_models, :dependent => :destroy
has_many :runways, :through => :runway_models
end
#controller
class MyModelController < ApplicationController
# ...
# params => {:my_model => {:runways_ids => ['1', '2', '3', '5', '8']}}
def update
respond_to do |format|
if #my_model.update_attributes(params[:my_model])
flash[:notice] = 'My Model was successfully updated.'
format.html { redirect_to(#my_model) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => #my_model.errors, :status => :unprocessable_entity }
end
end
end
# ...
end
This will end up triggering the after_create when new Runway records are associated, but will not trigger the before_destroy when a RunwayModel is deleted.
My question is...
Is there a way to make it work so that it will observe those changes (and/or potentially other deletes)?
Is there a better solution that is still relatively unobtrusive?
I had a similar requirement on a recent project. I ended using the acts_as_audited gem, and it worked great for us.
In my application controller I have line like the following
audit RunWay,RunWayModel,OtherModelName
and it takes care of all the magic, it also keeps a log of all the changes that were made and who made them-- its pretty slick.
Hope it helps
Use the Vestal versions plugin for this:
Refer to this screen cast for more details. Look at the similar question answered here recently.
Vestal versions plugin is the most active plugin and it only stores delta. The delta belonging to different models are stored in one table.
class User < ActiveRecord::Base
versioned
end
# following lines of code is from the readme
>> u = User.create(:first_name => "Steve", :last_name => "Richert")
=> #<User first_name: "Steve", last_name: "Richert">
>> u.version
=> 1
>> u.update_attribute(:first_name, "Stephen")
=> true
>> u.name
=> "Stephen Richert"
>> u.version
=> 2
>> u.revert_to(10.seconds.ago)
=> 1
>> u.name
=> "Steve Richert"
>> u.version
=> 1
>> u.save
=> true
>> u.version
=> 3
Added this monkey-patch to our lib/core_extensions.rb
ActiveRecord::Associations::HasManyThroughAssociation.class_eval do
def delete_records(records)
klass = #reflection.through_reflection.klass
records.each do |associate|
klass.destroy_all(construct_join_attributes(associate))
end
end
end
It is a performance hit(!), but satisfies the requirement and considering the fact that this destroy_all doesn't get called often, it works for our needs--though I am going to check out acts_as_versioned and acts_as_audited
You could also use something like acts_as_versioned http://github.com/technoweenie/acts_as_versioned
It versions your table records and creates a copy every time something changes (like in a wiki for instance)
This would be easier to audit (show diffs in an interface etc) than a log file