I've got a long controller method with a lot of redirect conditions:
def show
get_param_user
if params[:id].match(/\D/)
#document = Document.where(:user_id => #user.id, :issue => params[:id]).first
else
#document = Document.find(params[:id])
end
unless #document.blank?
unless #document.template.name == "Media"
unless #document.retired?
#creator = User.find(#document.user)
if #creator == #user # if document exists, based on name and id
#document.components.each do |a|
redirect_to share_error_url, :flash => { :error => "#{#document.title} contains retired content and is now unavailable." } if a.retired? and return
end
render #document.template.name.downcase.parameterize.underscore
end
else # if retired
redirect_to share_error_url, :flash => { :error => "That document has expired." } and return
end
else # if media
redirect_to share_error_url, :flash => { :error => "Media has no public link." } and return
end
else # if document doesn't exist
redirect_to share_error_url, :flash => { :error => "Can't find that document. Maybe check your link. Or maybe it was deleted. Ask #{#user.name}." } and return
end
end
As you might guess, it's prone to errors under certain conditions. Is there a neater way to rewrite it to make it more robust? I know methods should have only one render or redirect_to each, but I'm not sure how else to achieve what I need.
Thanks!
A few specific and little things.
First, in general, better don't use until with an else condition, and even less if you can use an if:
unless #document.blank?
is the same as
if #document.present?
Second, you use
#creator = User.find(#document.user)
when usually you can simply use:
#creator = #document.user
The semantics is a bit different (in the first case if #document.user is nil you'll immediatelly get an exception, in the secnond case not), but the second is what you commonly need.
Third, if it is sensible, you can move code to the model away from the controller, and use some nice enumerators:
def has_retired_components?
#document.components.any?(&:retired?)
end
Also, your controller method is not that complex. It is just
if #document.present? and #document.showable? # also #document.try(:showable?)
render whatever
else
redirect_to error_url, flash: { error: error_message }
end
error_message may be the result of a method call (on the object itself if it makes sense). That way you move the logic to verify if the oject is showable somewhere else where it is less muddled with rendering logic.
The problem is that if you have a showable? method and another to show error messages, you have to be sure that the business logic is always correct in both. An option is to treat it similarly to how validations work: have a method (lets call it with the horrible name showable_validation here just to go on) that returns a hash with the errors and messages (the reasons for the object not being showable, like {title: 'this is an error message'}. The showable? method would then be:
def showable?
showable_validation.empty?
end
and then you would also have in the model something like:
def showable_error
showable_validation.values.first
end
And that would be the error_message (#document.showable_error). That way, the logic is only in one method.
Related
This is more a style question than anything.
When writing queries, I always find myself checking if the result of the query is blank, and it seems - I dunno, overly verbose or wrong in some way.
EX.
def some_action
#product = Product.where(:name => params[:name]).first
end
if there is no product with the name = params[:name], I get a nil value that breaks things.
I've taken to then writing something like this
def some_action
product = Product.where(:name -> params[:name])
#product = product if !product.blank?
end
Is there a more succinct way of handling nil and blank values? This becomes more of a headache when things rely on other relationships
EX.
def some_action
#order = Order.where(:id => params[:id]).first
# if order doesn't exist, I get a nil value, and I'll get an error in my app
if !#order.nil?
#products_on_sale = #order.products.where(:on_sale => true).all
end
end
Basically, is there something I haven;t yet learned that makes dealing with nil, blank and potentially view breaking instance variables more efficient?
Thanks
If its just style related, I'd look at Rails' Object#try method or perhaps consider something like andand.
Using your example, try:
def some_action
#order = Order.where(:id => params[:id]).first
#products_on_sale = #order.try(:where, {:onsale => true}).try(:all)
end
or using andand:
def some_action
#order = Order.where(:id => params[:id]).first
#products_on_sale = #order.andand.where(:onsale => true).andand.all
end
Well even if you go around "nil breaking things" in your controller, you'll still have that issue in your views. It is much easier to have one if statement in your controller and redirect view to "not found" page rather than having several ifs in your views.
Alternatively you could add this
protected
def rescue_not_found
render :template => 'application/not_found', :status => :not_found
end
to your application_controller. See more here: https://ariejan.net/2011/10/14/rails-3-customized-exception-handling
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.
My 'create' function in my 'Message' controller is something like this:
def create
#message = Message.new(params[:message])
#message2 = Message.new(params[:message])
#message.sender_deleted = false
#message2.sender_deleted = true
if #message2.save
...
else
logger.debug("SAVE DIDN'T WORK")
For whatever reason, message2 cannot be saved, but #message can. I believe this is because you need to save only a variable named #message, but I can't figure out how to get around this. I need to, on this save, save multiple things to the database - is there some other way to do this or am I doing this completely wrong?
Thanks for your help
There's no reason you can't save more than once in an action, though why you'd want to do such a thing is debatable. You'll want to put the saves in a transaction so you only save when both records are valid. save! will raise an exception when the save fails.
def create
#message = Message.new(params[:message].merge(:sender_deleted=>false))
#message2 = Message.new(params[:message].merge(:sender_deleted=>true))
Message.transaction do
#message.save!
#message2.save!
end
redirect_to .... # handle success here
rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
# do what you need to deal with failed save here,
# e.g., set flash, log, etc.
render :action => :new
end
end
I'm using Inherited Resources for my Rails 2.3 web service app.
It's a great library which is part of Rails 3.
I'm trying to figure out the best practice for outputting the result.
class Api::ItemsController < InheritedResources::Base
respond_to :xml, :json
def create
#error = nil
#error = not_authorized if !#user
#error = not_enough_data("item") if params[:item].nil?
#item = Item.new(params[:item])
#item.user_id = #user.id
if !#item.save
#error = validation_error(#item.errors)
end
if !#error.nil?
respond_with(#error)
else
respond_with(#swarm)
end
end
end
It works well when the request is successful. However, when there's any error, I get a "Template is missing" error. #error is basically a hash of message and status, e.g. {:message => "Not authorized", :status => 401}. It seems respond_with only calls to_xml or to_json with the particular model the controller is associated with.
What is an elegant way to handle this?
I want to avoid creating a template file for each action and each format (create.xml.erb and create.json.erb in this case)
Basically I want:
/create.json [POST] => {"name": "my name", "id":1} # when successful
/create.json [POST] => {"message" => "Not authorized", "status" => 401} # when not authorized
Thanks in advance.
Few things before we start:
First off. This is Ruby. You know there's an unless command. You can stop doing if !
Also, you don't have to do the double negative of if !*.nil? – Do if *.present?
You do not need to initiate a variable by making it nil. Unless you are setting it in a before_chain, which you would just be overwriting it in future calls anyway.
What you will want to do is use the render :json method. Check the API but it looks something like this:
render :json => { :success => true, :user => #user.to_json(:only => [:name]) }
authorization should be implemented as callback (before_filter), and rest of code should be removed and used as inherited. Only output should be parametrized.Too many custom code here...
So I have a method in a reservation model called add_equip. This method does some checking to make sure the added piece of equipment is valid (not conflicting with another reservation).
The checks work. If a added piece of equipment shouldn't be added it isn't, and if it should it is.
The problem is I can't figure out how to send the messages back up to the controller to be put in the flash message? I know I must be missing something here, but I've googled for a few hours now and can't really find any clear explanations how how to pass errors back up the the controller, unless they are validation errors.
add_equip in reservations_controller
def add_equip
#reservation = Reservation.find(params[:id])
#addedEquip = Equip.find(params[:equip_id])
respond_to do |format|
if #reservation.add_equip(#addedEquip)
flash[:notice] = "Equipment was added"
format.html { redirect_to(edit_reservation_path(#reservation)) }
else
flash[:notice] = #reservation.errors
format.html { redirect_to(edit_reservation_path(#reservation)) }
end
end
end
add_equip in reservation model
def add_equip equip
if self.reserved.find_by_equip_id(equip.id)
self.errors.add_to_base("Equipment Already Added")
return false
elsif !equip.is_available?(self.start, self.end)
self.errors.add_to_base("Equipment Already Reserved")
return false
else
r = Reserved.new
r.reservation = self
r.equip = equip
r.save
end
end
Any help would be greatly appreciated. I know I'm missing something basic here.
Using add_to_base to store the error message seems fine to me, you just need to figure out how to get it into the view.
How about:
flash[:notice] = #reservation.errors.full_messages.to_sentence
Assuming you're going to re-display a form, you could also probably use:
<%= f.error_messages %>
Or possibly:
<%= error_messages_for :reservation %>
Also, you might want to use flash[:error], then you can color it differently with a CSS class in your view.
I think I can see why errors are not being passed back to the user.
The problem is that you are sending a redirect to the user when the action fails instead of just doing a render, that means you lose any variables you set up to use within the request. Instead of adding errors to the flash, just render the edit page and set the flash to a normal message and everything should be fine.
For example:
def add_equip
#reservation = Reservation.find(params[:id])
#addedEquip = Equip.find(params[:equip_id])
respond_to do |format|
if #reservation.add_equip(#addedEquip)
flash[:notice] = "Equipment was added"
format.html { redirect_to(edit_reservation_path(#reservation)) }
else
flash[:error] = 'Error adding equipment'
format.html { render :action => :edit }
end
end
end
Now you can continue to use the normal form helpers for displaying error messages.
Also, just a little suggestion for the model code, try to use i18n when possible (including for flash messages in the controller). Although this is mostly a personal preference, it gives a logical home to all your messages and specific text, and alos allows you to create general or default messages which can be changed in one place instead of duplicating the change in multiple models and controllers.
eg.
def add_equip equip
if self.reserved.find_by_equip_id(equip.id)
self.errors.add_to_base(:already_added)
return false
elsif !equip.is_available?(self.start, self.end)
self.errors.add_to_base(:already_reserved)
return false
else
r = Reserved.new
r.reservation = self
r.equip = equip
r.save
end
end