Ruby on Rails. Large condition in controller - ruby-on-rails

I'm faced a situation unfamiliar to me and I need advice.
There is a controller with the action 'update'
def update
#arrival = find_arrival
#details = #arrival.arrival_details
if check_conditions(#arrival)
flash[:notice] = 'Документ прихода отредактирован'
else
flash[:error] = 'Возникла ошибка. Проверьте правильность заполнения формы'
end
redirect_to edit_admin_arrival_path(#arrival)
end
and few private methods:
def check_conditions(arrival)
new_status = arrival_params[:status]
case #arrival.status
when 'draft'
return unless check_dependencies
recalculate_balance if new_status == 'accrued'
#arrival.update(arrival_params)
when 'canceled'
return unless new_status == 'draft'
#arrival.update(status: arrival_params[:status])
when 'accrued'
return if new_status == 'draft'
recalculate_balance if new_status == 'canceled'
#arrival.update(new_status)
end
end
def recalculate_balance
puts '[PRY] recalculated'
end
def check_dependencies
Provider.exists?(arrival_params[:provider_id]) &&
Warehouse.exists?(arrival_params[:warehouse_id])
end
I'm interested in the following - do I need to move this condition to a separate class or some Service Object for example? I do not think that this huge condition should be in the controller. What can you advise?

Definitely, it's not a Controller logic. Slim controllers are preferred. Better move this one to a service object, or to Arrival class and use as an arrival instance object method #arrival.check_conditions.
And I'd recommend to use state machine here: https://github.com/pluginaweek/state_machine

Related

undefined method `<<' for #<Answer::ActiveRecord_Relation:0x007fada31c7430>

Hi I create a controller Game to display a Q/A game
And I am blocked with <<, here is the code
def play
lvlup(lvl)
if lvl == 1
set_questions
else
get_questions
end
#answers = Answer.where.not(id: question.answer_id).limit(2).order("RANDOM()")
#answer ||= []
#answers << question.answer
#answers = #answers.shuffle
render 'play'
end
I create an array and I put the related answer in the global answers I want to display 4 Max.
Why does the undefined is here?
Here is the total code
class GamesController < ApplicationController
attr_accessor :lvl
def welcome
end
def congrat
end
def play
lvlup(lvl)
if lvl == 1
set_questions
else
get_questions
end
#answers = Answer.where.not(id: question.answer_id).limit(2).order("RANDOM()")
#answer ||= []
#answers << question.answer
#answers = #answers.shuffle
render 'play'
end
def loose
#question = Question.find(params[:question])
flash.now[:alert] = "Miss..."
render 'loose'
end
def check
#lvl = params[:lvl].to_i
answer_id = params[:id].to_i
question = Question.find(params[:question])
if #lvl == lvlmax
render action: 'congrat' and return
elsif answer_id == question.answer_id
flash.now[:notice] = "Well done !"
play
else answer_id != question.answer_id
loose
end
end
private
def lvlup(value)
#lvl = 1 + value.to_i
end
def lvlmax
#lvlmax = Question.all.count
end
def set_questions
#questionsids = []
Question.all.shuffle.each do |d|
#questionsids << d.id
end
cookies[:questions] = #questionsids
end
def get_questions
#questions = cookies[:questions].split('&')
end
def questions
#questions = cookies[:questions]
end
def question
#question = Question.find(questions[lvl])
end
end
Thank you for your help.
You are trying to append to the #answers result - this is an ActiveRecord relation, you cannot append data to that array.
Add .to_a in the end of your line where you set #answers to allow you to append to the array.
#answers = Answer.where.not(id: question.answer_id).limit(2).order("RANDOM()").to_a
mtrolle's answer might be correct, but I have my doubts as to why ActiveRecord::Relation was not returned as Array by default. (Also as mentioned by BroiStatse in his comment.)
I too noticed the same problem with my local setup however it was attributed to another issue all together. I am sharing this here in case you too happen to use MySQL.
Answer.where.not(id: question.answer_id).limit(2).order("RANDOM()")
returns an ActiveRecord::Relation object. And it translates to following SQL:
SELECT `answers`.* FROM `answers` WHERE (id != ID) ORDER BY RANDOM() LIMIT 2
When I try running the same in MySQL, I get:
ERROR 1305 (42000): FUNCTION database.RANDOM does not exist
Apparently MySQL does not have RANDOM() function, instead it uses RAND().
Converting ActiveRecord query accordingly returned correct Array to me:
Answer.where.not(id: question.answer_id).limit(2).order("RAND()")

better way to build association in controller

I need a link in a show method of a parent class for creating associated models, so I have the code:
link_to "incomplete", new_polymorphic_path(part_c.underscore, :survey_id => survey.id)
in a helper.
This links to a part, which has new code like this:
# GET /source_control_parts/new
def new
get_collections
if params[:survey_id]
#s = Survey.find(params[:survey_id])
if #s.blank?
#source_control_part = SourceControlPart.new
else
#source_control_part = #s.create_source_control_part
end
else
#source_control_part = SourceControlPart.new
end
end
I know this is not very DRY. How can I simplify this? Is there a RAILS way?
How about this:
def new
get_collections
get_source_control_part
end
private
def get_source_control_part
survey = params[:survey_id].blank? ? nil : Survey.find(params[:survey_id])
#source_control_part = survey ? survey.create_source_control_part : SourceControlPart.new
end

How to handle multiple conditions of instance variable assignment

I have the following in my controller that will assign a different collection of results depending on what params are received with an Ajax call. It is messy and i would like to just call a function with all the logic in rather than all this in my index controller
class PublicController < ApplicationController
def index
if params[:literacy_param].present?
#skills = Skill.search(params)
elsif params[:numeracy_param].present?
#skills = Skill.numeracy_default_params
elsif params[:numeracy_number_skills].present?
#skills = Skill.numeracy_number_skills
elsif params[:numeracy_measuring_skills].present?
#skills = Skill.numeracy_measuring_skills
elsif params[:numeracy_data_skills].present?
#skills = Skill.numeracy_data_skills
else
#skills = Skill.default_params
end
end
end
Im just a bit unsure on how to set out my function so that it can read the params that are being sent,
I have come up with this so far
private
def skills(params)
if params[:literacy_param].present?
#skills = Skill.search(params)
elsif params[:numeracy_param].present?
#skills = Skill.numeracy_default_params
elsif params[:numeracy_number_skills].present?
#skills = Skill.numeracy_number_skills
elsif params[:numeracy_measuring_skills].present?
#skills = Skill.numeracy_measuring_skills
elsif params[:numeracy_data_skills].present?
#skills = Skill.numeracy_data_skills
else
#skills = Skill.default_params
end
end
Then in my index action i would do
#skills = skills(params)
would this be an efficient way?
Thanks
You can do this
class PublicController < ApplicationController
def index
skills = ['literacy_param', 'numeracy_param', 'numeracy_number_skills', 'numeracy_measuring_skills', 'numeracy_data_skills']
common_in_params = (skills & params).first
#skills = common_in_params.present? ? (common_in_params.eql?('literacy_param') ? Skill.search(params) : Skill.send(common_in_params)) : Skill.default_params
end
end
You can define skills array in an initializer for resusability
One way of doing it would be this:
def skills(params)
set_of_skills = params.slice(
:numeracy_param,
:numeracy_number_skills,
:numeracy_measuring_skills,
:numeracy_data_skills,
).first
#skills = if params[:literacy_param]
Skill.search(params)
elsif set_of_skills
Skill.public_send(set_of_skills)
else
Skill.default_params
end
end
I would also advise to have this extracted into a lib/ folder, and unit-tested. So that in your controller you could perform the following:
def index
#skills = SkillSearch.new(params).search
end
Two ways I can think of doing this right now:
Wrap the params in a unique key. As in params = { :keyword => :literacy_param }, and then use this unique key to identify the right operation.
In you skill.rb:
def self.filter(params)
if params[:keyword] == :literacy_param
search(params)
elsif available_filters.include?(params[:keyword])
public_send(params[:keyword])
else
default_params
end
end
private
def self.available_filters
%i{numeracy_default_params numeracy_number_skills numeracy_measuring_skills numeracy_data_skills}
end
considering that instead of :numeracy_param, you send :numeracy_default_params in :keyword key. Otherwise you'll have to make another elsif inside filter method.
then in your index method:
def index
#skilles = Skill.filter(params)
end
You create a separate filter class, which is an expandable solution, just in case when you need to go for complex search queries and filtering.
Let's call it SkillSeacrher, inside you app/models/skill_searcher.rb:
class SkillSearcher
attr_reader :keyword
def initialize(keyword)
#keyword = keyword
end
def filter
if keyword == :literacy_param
Skill.search(params)
elsif available_filters.include?(keyword)
Skill.public_send(keyword)
else
Skill.default_params
end
end
private
def self.available_filters
%i{numeracy_default_params numeracy_number_skills numeracy_measuring_skills numeracy_data_skills}
end
end
then in index method:
def index
#skills = SkillSearcher.new(params[:keyword]).filter
end
However, you can do one more change to filter method(depends on your taste):
def filter
if keyword == :literacy_param
Skill.search(params)
else
Skill.public_send(available_filters.include?(keyword) ? keyword : :default_params)
end
end
And, if you have all these methods accepting params as arguments then it'd be much more sleek:
def filter
Skill.public_send(available_filters.include?(keyword) ? keyword : :default_params, params)
end

Saving to DB from Model.rb

When an new order_preview is created, I call USPS for shipping options. If a user updates their zip, I would like the ship_option to reset
Edit: I am no longer calling the intial API call from the view, rather I do an after_create method in the controller.
def get_ship_options
ship_options = {}
#order_preview.fedex_rates.each do |k, v|
if k.service_name == "FedEx Ground Home Delivery" || k.service_name == "FedEx 2 Day" || k.service_name == "FedEx Standard Overnight"
ship_options["#{k.service_name}"] = "#{number_to_currency(k.price.to_f / 100)}"
end
end
#order_preview.usps_rates.each do |k, v|
if k.service_name == "USPS Priority Mail 1-Day"
ship_options["#{k.service_name}"] = "#{number_to_currency(k.price.to_f / 100)}"
end
end
#order_preview.ship_option_hash = ship_options.map { |k,v| ["#{k} - #{v}","#{k} - #{v}" ] }
#order_preview.save
end
I tried using the answers you guys provided, but the before_save didn't actually save the shiphash the way #order_preview.save does at the end of the above method.
I tried using the same idea, but zip_changed? doesn't work in the controller.
How can I save the new hash that is pulled from the model directly over to the #order_preview ?
From the model I now have
Model.rb
def clear_hash
if zip_changed?
get_shipping_rates
end
end
and
ship_options = {}
fedex_rates.each do |k, v|
if k.service_name == "FedEx Ground Home Delivery" || k.service_name == "FedEx 2 Day" || k.service_name == "FedEx Standard Overnight"
ship_options["#{k.service_name}"] = "#{number_to_currency(k.price.to_f / 100)}"
end
end
usps_rates.each do |k, v|
if k.service_name == "USPS Priority Mail 1-Day"
ship_options["#{k.service_name}"] = "#{number_to_currency(k.price.to_f / 100)}"
end
end
ship_option_hash = ship_options.map { |k,v| ["#{k} - #{v}","#{k} - #{v}" ] }
**save ship_option_hash to #order_preview.ship_option_hash**
class OrderPreview
before_save :check_state
def check_state
if zip_changed?
ship_option_hash = nil
end
end
...
end
class OrderPreviewController
def update
#order_preview.update(order_preview_params)
end
...
end
Try changing your callback from after_save to before_save. Record considered changed until the changes are not persisted. Changes are lost when you save your object, that's why your record is unchanged when you check for changes in after_save callback.
It should work this way:
before_save :clear_hash, if: :zip_changed?
def clear_hash
ship_option_hash = nil
end
This way the changes will be saved, because you use before_save. In your code, changes were not saved, because you used after_save callback
You controller:
def update
respond_to do |format|
if #order_preview.update(order_preview_params)
flash[:notice] = "Record was successfully updated"
else
flash[:alert] = "Record was not updated"
end
end
end

what is complex in this method

I have this action in my rails controler,
def step_submit
validate_user()
#owning = #user.create_user_car_transaction(Variant.find(params[:variant]), params[:details], params[:address], params[:somethin1])
Contact.user_contact(current_user, params[:contact]) if #user.contact.nil?
redirect_to "/next_step"
end
I use codeClimate to check the quality of the code..
it shows this action's complexity ~ 30 ..
I actually broke a really huge method into this.. how can i still reduce this complexity?
these are the different methods the action calls
def self.user_contact(user, contact_hash = nil)
contact = user.contact || user.create_contact()
contact.update_attributes(contact_hash) if contact_hash.present?
contact
end
def validate_user
if params[:user] && current_user.nil?
user = User.create(params[:user])
sign_in user
end
end
def create_user_car_transaction(car, details_hash, address_hash, coupon_hash = nil)
transaction = self.transactions.create()
car.transaction_item = transaction.transaction_items.create()
car.save
payment_hash = details_hash
payment_hash.merge!(address_hash)
payment = transaction.create_payment(payment_hash)
transaction.update_attributes(:status=>"1") if transaction.status.nil?
transaction
end

Resources