Refactoring a similar controller's action - ruby-on-rails

I used Pull Review for reviewing my app's code and it came back with this:
Consider refactoring, similar code detected.
Occurred at:
SkillsController # index
PagesController # index
So the app/controllers/skills_controller.rb index action code is:
def index
#skill = Skill.new
if params[:search]
#skills = Skill.search(params[:search]).order('created_at DESC')
else
#skills = Skill.all.order('created_at DESC')
end
end
and on app/controllers/pages_controller.rb is:
def index
#users = User.all
if params[:search]
#users = User.search(params[:search]).order('created_at DESC')
else
#users = User.all.order('created_at DESC')
end
end
Am I suppose to somehow refactor these two actions on these two controllers? Also, I am not sure how I refactor this. Do I extract the if params[:search] segment and replace the instance variables with another variable that will be used on both actions?
Thanks for your time.

I don't know where your method search comes from. It seems it comes from a custom module/gem for ActiveRecord.
If so, you can change the method to shorten code in controller
def self.search(args)
return self unless args
original_search_logic args
end
# As well as extract order to a scope
scope :by_time, -> { order('created_at DESC') }
Then in controller:
# Skill
def index
#skills = Skill.search(params[:search]).by_time
end
# User
def index
#users = User.search(params[:search]).by_time
end
These should be dry enough for now.

take a look at the has_scope and inherited_resources. You can extract the params[:search] part with has_scope. And use inherited_resources to extract how to get the collection and do the ordering.

Related

How to use math in controller variables

I simply need to be able to use math to set the end point of a query in my controller.
class WelcomeController < ApplicationController
def index
#video = Video.last
#videos = Video.last(7).reverse!.drop(1)
end
def show
#video = Video.find(params[:id])
#videos = Video.where(:id => start..stop)
end
end
This line: #videos = Video.where(:id => start..stop) should be something like #videos = Video.where(:id => params[:id]..params[:id]-7) because that array is supposed to be the next seven database entries after #video.
I'm also certain there's a better way to accomplish what I'm trying to do but I have no idea what it is.
One way to solve this is using a gem called will_paginate, which will automatically do what you are trying to solve
Example would be
#videos = Video.paginate(page: params[:page] || 1, per_page: 5)
you can get the page from the params, so your web page has to request something like this /videos?page=2
Other gem in this category is Kaminari

How do I create a variable in the controller for single or multiple params

def index
#users = User.all.paginate(page: params[:page])
#users = User.named(params[:name]).paginate(page: params[:page]) if params[:name].present?
#users = User.countryname(params[:country]).paginate(page: params[:page]) if params[:country].present?
#users = User.gender(params[:gender_type]).paginate(page: params[:page]) if params[:gender_type].present?
end
The following code works fine if only :name or :country or :gender_type is present. But it does not work if multiple params are present. What is the DRY way of writing this code for multiple params? Obviously, I do not want to create a different line of code for each possible combination of params.
Here are the scopes:
class User
scope :countryname, -> (country) { where("country ILIKE ?", "%#{country}%")}
scope :gender, -> (gender_type) { where gender_type: gender_type}
scope :named, -> (name) { where("name ILIKE ?", "%#{name}%")}
If I have a query string of
example.com/users?name=sam&gender_type=male
it simply returns all users with names like sam and ignores their gender... I would need to code:
#users = User.gender(params[:gender_type]).named(params[:name]).paginate(page: params[:page]) if params[:gender_type] && params[:name].present?
but I do not want to have to write a new line of code for every single combination of parameters.
You could use the ruby try method. For example, you could write something like
#users = User.try(:gender, params[:gender_type]).try(:paginate, page: params[:page])
Look at try in api docs for other ways to use it.
the problem was the code should be
#users = User.all.paginate(page: params[:page])
#users = #users.named(params[:name]).paginate(page: params[:page]) if params[:name].present?
etc
Rails will then allow chain scoping automatically. the previous code creates separate independent non-chained instance variables.

Slimming up scoped filters in rails controller

I'm putting filtering functionality into an application (Rails 4.1beta) - I did this by creating scopes on the Item model, passing a scope through the request params and doing a case statement in the index action. It all works but there's a code smell I'm trying to get rid of in the index action of one of the controllers;
def index
case params[:scope]
when "recent"
#items = Item.recent
when "active"
#items = Item.active
when "inactive"
#items = Item.inactive
else
#items = Item.all
end
end
It all feels a little too rigid / verbose. I'd really like to just do something like this;
def index
#items = Item.send(params[:scope])
end
but then I leave the application wide open to people calling methods on the Item class. Whacking conditions in there kinda defeats the point of what I'm trying to achieve.
Is there some rails magic I'm missing that can help me here?
You can use different controllers to do each of these.
inactive_items_controller.rb
def index
#items = Item.inactive
end
recent_items_controller.rb
def index
#items = Item.recent
end
etc.
Or you can just move you logic you have above to the model
Item model
def self.custom_scope(scope)
case scope
when "recent"
Item.recent
when "active"
Item.active
when "inactive"
Item.inactive
else
Item.all
end
end
or
def self.custom_scope(scope)
recent if scope == 'recent'
active if scope == 'active'
inactive if scope == 'inactive'
scoped if scope.blank?
end
And then in your index
#items = Item.custom_scope params[:scope]
something like this:
if ['recent', 'active', 'inactive'].include?(params[:scope])
#items = Item.send(params[:scope])
else
#items = Item.all
end

Rails 4 json return on API

I'm creating an API on my application. I currently overrided the as_json method in my model in order to be able to get attached files as well as logo from Paperclip :
def as_json( options = {} )
super.merge(logo_small: self.logo.url(:small), logo_large: self.logo.url(:large), taxe: self.taxe, attachments: self.attachments)
end
Then within my controller, I'm doing :
def index
#products = current_user.products
respond_with #products
end
def show
respond_with #product
end
The problem is that on the index, I don't want get all the attachments. I only need it on the show method. So I tried it :
def index
#products = current_user.products
respond_with #products, except: [:attachments]
end
But unfortunately it's only working on default product attributes (everyting that I merged seems not to be consider). How can I do to not send :attachments?
Thanks
I'd recommend you have a look at active_model_serializers. It will provide a nice and OOP way of handling the kind of object decoration you need - selectively excluding attributes - and much more. There's even a Railscast!

Getting "Not In" to Work in Rails MySQL Query

I have three models - User. Game, and Activity. I have a view that is supposed to show games where the user has no Activity records. I'm having trouble figuring out how to write the query that excludes Games that already have activity.
When I do these two versions (in the Controller), it does not work (I receive the error undefined method 'game_id' for #<ActiveRecord::Relation:0x69eab40>)
version 1
def index
if current_user
#activities = current_user.activities.game_id
#games = Game.where("id NOT IN (?)", #activities )
end
end
version 2
def index
if current_user
#activities = current_user.activities
#activity_ids = #activities.game_id
#games = Game.where("id NOT IN (?)", #activity_ids )
end
end
However, when I do this it works:
The controller:
def index
if current_user
#activities = current_user.activities
#games = Game.where("id NOT IN (?)", #activities.collect {|p| p.game_id} )
end
end
I'm worried about doing it this way because I'm not sure how scalable it is. I got the idea to do it from this [question], but in along with the answers people said that this solution was not scalable (which was not an issue for that question asker).1
your error is not in that line. what's causing your error is this
#activities = current_user.activities.game_id
current_user.activities is an ActiveRecord::Relation object so calling game_id on it raises an exception. Your solution that works is fine since you're going to use #activities (you're going to, right?). If you want a more sql approach, try
def index
if current_user
#activities = current_user.activities
#games = Game.where("id NOT IN (?)", #activities.uniq.pluck(:game_id))
end
end

Resources