Rails: Run callback only for CRUD operation? - ruby-on-rails

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.

Related

How to Refactor Controller Code To Model Code

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.

Rails: Conditional Validation & Views

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.

How would I get an error to display with this controller/model setup?

I have a create method that calls a method in a model that pings some third-party APIs.
What I need to do is if the API sends back a certain message, then I'd display an error.
Below is my current controller and model setup, so how would I get the error back in to the controller (and ultimately the view)?
Here is the method in my controller:
def create
#number = Number.where(:tracking => params[:number][:tracking], :user_id => current_user.id).first
if #number.blank?
#number = Number.new
#number.tracking = params[:number][:tracking]
#number.user_id = current_user.id
#number.notes = params[:number][:notes]
#number.track
end
respond_with(#number) do |format|
format.html { redirect_to root_path }
end
end
Here are the methods in my model:
def track
create_events
end
def create_events(&block)
tracker = fedex.track(:tracking_number => number)
if tracker.valid?
self.assign_tracker(tracker)
tracker.events.each do |e|
self.create_event(e) unless (block_given? && !block.call(e))
end
save
else
# NEED TO THROW THE ERROR HERE
end
end
How about if rather than throwing errors, you just use validation? Something like the following (Just to get your started. This would need work.):
# if you don't cache the tracker in an attribute already, do this so
# you can add errors as if it were a column.
attr_accessor :tracker
def create_events(&block)
tracker = fedex.track(:tracking_number => number)
if tracker.valid?
# ...
else
# add the error with i18n
errors.add(:tracker, :error_type_if_you_know_it)
# or add it from a returned message
errors.add(:tracker, nil, :message => fedex.get_error())
end
end
Then in your controller:
#number.track
respond_with(#number) do |format|
if #number.errors.any?
format.html { redirect_to root_path }
else
format.html { render :some_template_with_errors }
end
end
Alternatively you could do this as part of validation (so calling valid? would work as expected and not destroy your custom "track" errors)
# do your tracking on creation, if number was given
validate :on => :create do
if number.present?
tracker = fedex.track(:tracking_number => number)
unless tracker.valid?
errors.add :tracker, nil, :message => tracker.get_error()
end
end
end
# then do your actual creation of tracking events sometime after validation
before_save :handle_tracker_assignment
def handle_tracker_assignment
self.assign_tracker(tracker)
# note the block method you're using would need to be reworked
# ...
end
Note in the latter case you'd have to change your logic a bit, and simply pass the tracking number and attempt to save a new record, which would trigger the tracking attempt.
You should typically offload the the API calls to a background job and you could either use notifiers (or Rack middleware) to raise self-defined errors and handle them accordingly.

How to create a full Audit log in Rails for every table?

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

Ruby on Rails: Equating items in controllers (or maybe models?)

I'm trying to make attributes equal predetermined values, and I'm not sure if I'm doing that efficiently with the following (in my orders controller):
def create
#order = Order.find(params[:id])
#order.price = 5.99
#order.representative = Product.find(params[:product_id]).representative
#order.shipping_location = SHIPPING_LOCATION
#order.user = current_user
respond_to do |format|
...
end
end
Is there a more efficient way to equate attributes in Rails (maybe using models)? If I'm using two different controllers, do I just repeat what I did above for the new controller?
Use before_create callback in model to assign default values.
Your code is a little off, it looks like a controller action for create, but the code reads like it's for an update.
Regardless...
You could use a parameter hash to update everything at once.
In the case where you're creating:
order_update = {:price => 5.99, :representative =>
Product.find(params[:product_id]).representative,
:shipping_location => SHIPPING_LOCATION,
:user => current_user}
#order = Order.new(order_update)
In the case where you're updating:
#order.update_attributes(order_update) #attempts to save.
Mixing it into your controller code we get:
def create
#order = Order.find(params[:id])
order_update = {:price => 5.99, :representative =>
Product.find(params[:product_id]).representative,
:shipping_location => SHIPPING_LOCATION,
:user => current_user}
respond_to do |format|
if #order.update_attributes(order_update)
# save succeeded. Redirect.
else
# save failed. Render with errors.
end
end
end
Another solution:
class Example < ActiveRecord::Base
DEFAULTS = HashWithIndifferentAccess.new(:some => 'default', :values => 'here')
def initialize(params = {})
super(DEFAULTS.merge(params))
end
end
Either use initialize and merge with params, or use an ActiveRecord hook like before_create etc.

Resources