Rails controller refactoring - ruby-on-rails

I have code similar to follwoing
...
#c = M.find(params[:id]).c.find(params[:c_id])
if #c.s.count > 0
#s = #c.s.sort_by{|e| e[:order]}.first
unless #s.p.nil?
#img = #s.p.image_file.remote_url
else
#s.p = P.new
#img = request.protocol + request.host_with_port + "/none.png"
end
unless #s.a.nil?
#voice = #s.a.audio_file.remote_url
else
#s.a = A.new
end
else
...
end
#c_v_url = ""
unless #c_v_url.nil?
#c_v_url = #c.v_o.a_file.remote_url
else
#c.v_o = A.new
end
#c_m_url = ""
unless #c_m_url.nil?
#c_m_url = #c.b_m.a_file.remote_url
else
#c.b_m = A.new
end
...
Now all the instance variables are to be used in the view and I want to re-factor the code to make the Controller skinny. What will be the best approach to do the re-factoring? Will it be wise to move this code to the Model?

I can't really see what this code is used for, but it looks like view logic to display images, file and audio links?
I'd create a view helper method for each one, for example:
def s_image_url(s)
unless s.p.nil?
s.p.image_file.remote_url
else
request.protocol + request.host_with_port + "/none.png"
end
end
For more info on view helpers

I would use the Presenter Pattern, here are some resources for explanation (there are a lot more out there):
http://blog.jayfields.com/2007/03/rails-presenter-pattern.html
http://kpumuk.info/ruby-on-rails/simplifying-your-ruby-on-rails-code
Short story: You put all your logic for retrieving your models in the presenter. The presenter is easy to test and extensible. In your controller action will have only one line of code to instantiate the presenter.

Related

Ruby on Rails. Large condition in controller

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

Rails Admin routes inside config

I'm using Rails Admin for my admin area.
The sidebar panel should have some links to the instances of a model.
In rails_admin.rb I've tried something like:
RailsAdmin.config do |config|
#navigation_links = Hash[*Conference.all.map {|conference| [conference.name, bindings[:view].main_app.show_path(model_name: 'conference', id: conference.id)]}.flatten]
config.navigation_static_links = #navigation_links
end
However, here I do not have access to bindings. So, how can I get the url of an admin resource here? I cannot see it in the documentation
Thanks
My answer probably not what you want to do, but it can be helpful.
I've checked rails_admin.gem and i found that there are two methods that responsible for rendering sidebar menu.
def main_navigation
nodes_stack = RailsAdmin::Config.visible_models(controller: controller)
node_model_names = nodes_stack.collect { |c| c.abstract_model.model_name }
nodes_stack.group_by(&:navigation_label).collect do |navigation_label, nodes|
nodes = nodes.select { |n| n.parent.nil? || !n.parent.to_s.in?(node_model_names) }
li_stack = navigation nodes_stack, nodes
label = navigation_label || t('admin.misc.navigation')
%(<li class='dropdown-header'>#{capitalize_first_letter label}</li>#{li_stack}) if li_stack.present?
end.join.html_safe
end
Method above responsible for getting list of models, especially:
nodes_stack = RailsAdmin::Config.visible_models(controller: controller)
Second method responsible for rendering item in the menu (aka li):
def navigation(nodes_stack, nodes, level = 0)
nodes.collect do |node|
model_param = node.abstract_model.to_param
url = url_for(action: :index, controller: 'rails_admin/main', model_name: model_param)
level_class = " nav-level-#{level}" if level > 0
nav_icon = node.navigation_icon ? %(<i class="#{node.navigation_icon}"></i>).html_safe : ''
li = content_tag :li, data: {model: model_param} do
link_to nav_icon + capitalize_first_letter(node.label_plural), url, class: "pjax#{level_class}"
end
li + navigation(nodes_stack, nodes_stack.select { |n| n.parent.to_s == node.abstract_model.model_name }, level + 1)
end.join.html_safe
end
So you can patch this methods to get what you need.
module RailsAdmin
module ApplicationHelper
def main_navigation
# your code
end
end
end
rails_admin.gem module
P.S. I love what you can read from rails doctrine about monkey patching:
This power has frequently been derided as simply too much for mere
mortal programmers to handle.

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

Rspec Ruby on Rails Test File System in model

I have a model that has a method that looks through the filesystem starting at a particular location for files that match a particular regex. This is executed in an after_save callback. I'm not sure how to test this using Rspec and FactoryGirl. I'm not sure how to use something like FakeFS with this because the method is in the model, not the test or the controller. I specify the location to start in my FactoryGirl factory, so I could change that to a fake directory created by the test in a set up clause? I could mock the directory? I think there are probably several different ways I could do this, but which makes the most sense?
Thanks!
def ensure_files_up_to_date
files = find_assembly_files
add_files = check_add_assembly_files(files)
errors = add_assembly_files(add_files)
if errors.size > 0 then
return errors
end
update_files = check_update_assembly_files(files)
errors = update_assembly_files(update_files)
if errors.size > 0 then
return errors
else
return []
end
end
def find_assembly_files
start_dir = self.location
files = Hash.new
if ! File.directory? start_dir then
errors.add(:location, "Directory #{start_dir} does not exist on the system.")
abort("Directory #{start_dir} does not exist on the system for #{self.inspect}")
end
Find.find(start_dir) do |path|
filename = File.basename(path).split("/").last
FILE_TYPES.each { |filepart, filehash|
type = filehash["type"]
vendor = filehash["vendor"]
if filename.match(filepart) then
files[type] = Hash.new
files[type]["path"] = path
files[type]["vendor"] = vendor
end
}
end
return files
end
def check_add_assembly_files(files=self.find_assembly_files)
add = Hash.new
files.each do |file_type, file_hash|
# returns an array
file_path = file_hash["path"]
file_vendor = file_hash["vendor"]
filename = File.basename(file_path)
af = AssemblyFile.where(:name => filename)
if af.size == 0 then
add[file_path] = Hash.new
add[file_path]["type"] = file_type
add[file_path]["vendor"] = file_vendor
end
end
if add.size == 0 then
logger.error("check_add_assembly_files did not find any files to add")
return []
end
return add
end
def check_update_assembly_files(files=self.find_assembly_files)
update = Hash.new
files.each do |file_type, file_hash|
file_path = file_hash["path"]
file_vendor = file_hash["vendor"]
# returns an array
filename = File.basename(file_path)
af = AssemblyFile.find_by_name(filename)
if !af.nil? then
if af.location != file_path or af.file_type != file_type then
update[af.id] = Hash.new
update[af.id]['path'] = file_path
update[af.id]['type'] = file_type
update[af.id]['vendor'] = file_vendor
end
end
end
return update
end
def add_assembly_files(files=self.check_add_assembly_files)
if files.size == 0 then
logger.error("add_assembly_files didn't get any results from check_add_assembly_files")
return []
end
asm_file_errors = Array.new
files.each do |file_path, file_hash|
file_type = file_hash["type"]
file_vendor = file_hash["vendor"]
logger.debug "file type is #{file_type} and path is #{file_path}"
logger.debug FileType.find_by_type_name(file_type)
file_type_id = FileType.find_by_type_name(file_type).id
header = file_header(file_path, file_vendor)
if file_vendor == "TBA" then
check = check_tba_header(header, file_type, file_path)
software = header[TBA_SOFTWARE_PROGRAM]
software_version = header[TBA_SOFTWARE_VERSION]
elsif file_vendor == "TBB" then
check = check_tbb_header(header, file_type, file_path)
if file_type == "TBB-ANNOTATION" then
software = header[TBB_SOURCE]
else
software = "Unified"
end
software_version = "UNKNOWN"
end
if check == 0 then
logger.error("skipping file #{file_path} because it contains incorrect values for this filetype")
asm_file_errors.push("#{file_path} cannot be added to assembly because it contains incorrect values for this filetype")
next
end
if file_vendor == "TBA" then
xml = header.to_xml(:root => "assembly-file")
elsif file_vendor == "TBB" then
xml = header.to_xml
else
xml = ''
end
filename = File.basename(file_path)
if filename.match(/~$/) then
logger.error("Skipping a file with a tilda when adding assembly files. filename #{filename}")
next
end
assembly_file = AssemblyFile.new(
:assembly_id => self.id,
:file_type_id => file_type_id,
:name => filename,
:location => file_path,
:file_date => creation_time(file_path),
:software => software,
:software_version => software_version,
:current => 1,
:metadata => xml
)
assembly_file.save! # exclamation point forces it to raise an error if the save fails
end # end files.each
return asm_file_errors
end
Quick answer: you can stub out model methods like any others. Either stub a specific instance of a model, and then stub find or whatever to return that, or stub out any_instance to if you don't want to worry about which model is involved. Something like:
it "does something" do
foo = Foo.create! some_attributes
foo.should_receive(:some_method).and_return(whatever)
Foo.stub(:find).and_return(foo)
end
The real answer is that your code is too complicated to test effectively. Your models should not even know that a filesystem exists. That behavior should be encapsulated in other classes, which you can test independently. Your model's after_save can then just call a single method on that class, and testing whether or not that single method gets called will be a lot easier.
Your methods are also very difficult to test, because they are trying to do too much. All that conditional logic and external dependencies means you'll have to do a whole lot of mocking to get to the various bits you might want to test.
This is a big topic and a good answer is well beyond the scope of this answer. Start with the Wikipedia article on SOLID and read from there for some of the reasoning behind separating concerns into individual classes and using tiny, composed methods. To give you a ballpark idea, a method with more than one branch or more than 10 lines of code is too big; a class that is more than about 100 lines of code is too big.

Rails server hangs when a User method I created is called. No errors and I don't know how to test it

I call this method (with helper method detailed below as well), defined in User.rb model
def get_random_items
return nil unless groups_as_member
if groups_as_member == 1
assignments = groups_as_member.assignments.limit(5)
random_items = Post.rand_by_post(assignments)
end
random_groups = groups_as_member.sort_by{rand}.slice(0,5)
random_items = Array.new
i=0
return unless random_groups
until i == 10 do
random_groups.each do |group|
assignments = group.assignments.limit(5)
if y = Post.rand_by_post(assignments)
random_items << y
i+=1
if random_items == 5
return random_items
end
else
return random_items
end
end
end
return random_items
end
helper method rand_by_post in Post.rb
def self.rand_by_post(assignments)
find_by_id(assignments.rand.post_id)
end
in the user controller show action:
def show
#public_groups = Group.public
#groups_member = #user.groups_as_member
#groups_as_owner = #user.groups_as_owner
#random_items = #user.get_random_items
end
when I comment out the call in the user show action, the user show works fine on my development and production server. But when I try to user the method the servers will just hang there, not doing anything. I can't find any errors in the server logs or the heroku logs. My test writing skills are pretty limited, and I am having trouble writing one for the entire method.
Can anyone spot a problem?
if your random_groups is empty, your helper method get_random_items will go into an endless loop until i == 10 do ... end. That could be the reason.
You might want to change return unless random_groups to return if random_groups.empty?

Resources