Rails: collection based on params - ruby-on-rails

I had an index method that looked, awfully, like this:
def index
if params[:brand]
#users = User.includes(:brand).where(brand_id: params[:brand]).order("#{sort_column} #{sort_direction}").page(params[:page]).per(10)
elsif params[:search]
#user = User.includes(:brand).find_by_client_code(params[:search])
redirect_to edit_user_path(#user)
elsif params[:page] == 'all'
#users = User.includes(:brand).order("#{sort_column} #{sort_direction}").all
elsif params[:state]
#users = User.includes(:brand).where(state: params[:state]).page(params[:page]).per(10)
else
#users = User.includes(:brand).order("#{sort_column} #{sort_direction}").page(params[:page]).per(10)
end
end
Pretty messy, I know, but it worked. Now I'm trying to refactor it and I can't figure out the best way to split it into smaller collections without complicating my routes.
def index
[:brand, :page, :search, :state].each do |param|
if params[:page] == 'all'
#users = User.includes(:brand).order(column + ' ' + direction)
elsif params.key?(param)
param
else
#users = User.includes(:brand).order(column + ' ' + direction)
.page(params[:page]).per(10)
end
end
end
def brand
#users = User.includes(:brand).where('brand_id in (?)', params[:brand])
.order(column + ' ' + direction).page(params[:page]).per(10)
end
def state
#users = User.includes(:brand).where(state: params[:state])
.page(params[:page]).per(10)
end
def search
#user = User.includes(:brand).find_by_client_code(params[:search])
redirect_to edit_user_path(#user)
end
The above doesn't work but you get the idea. Anyone know a good way to deal with this type of situation? Cheers.

I'd probably do it this way -
First, update this code where you have defined sort_column and sort_direction methods to have default values:
def sort_column
colum_name = params[:colum_name]
colum_name ||= 'id'
end
def sort_direction
direction = params[:direction]
direction ||= 'ASC'
end
Add a new method to have per_page(at the same place where you have sort_column and sort_direction) from params or default from User class:
def per_page
per = params[:per_page]
per ||= User.per_page
end
in app/models/user.rb:
scope :with_brand_id, ->(id) { where(brand_id: id) }
scope :with_state, ->(state) { where(state: state) }
scope :order_with, ->(column_name, direction) { order("#{sort_column} #{sort_direction}") }
# never use/avoid magic numbers in your application at multiple places as they gets unmanageable as your application grows
# single place to manage your per page entries for users.
def self.per_page
10
end
# easy to use key:value based arguments since you're using Ruby 2, cheers!
def self.fetch_with_brand(brand: nil, state: nil, page: nil, sort_column: 'id', sort_direction: 'ASC', per_page: User.per_page)
user_scope, pagination_scope_applies = if brand.present?
[self.with_brand_id(brand), true]
elsif state.present?
[self.with_state(state), true]
else
[self.scoped, (page != 'all')]
end
user_scope.merge(pagination_scope(page, per_page)) if pagination_scope_applies
user_scope.includes(:brand).order_with(sort_column, sort_direction)
end
# since I am not sure about your implementation of `page` and `per` methods, I'd create a class method, otherwise you can create a `scope` for this, too
def self.pagination_scope(page_number, per_page)
self.page(page_number).per(per_page)
end
Do you se the line: [self.scoped, (page != 'all')] in code mentioned above? Here self.scoped is equal to self.all(when evaluated), but we will have to use scoped instead of all as in Rails 3 it gives an Array, while in Rails 4 it will be an ActiveRecord::Relation object so you can use self.all if you're on Rails 4. NOTE: scoped is deprecated in Rails 4 in favor of all.
Also, I'd like to point out a gotcha here. In your code you're giving priority to params[:page] == 'all' condition and then to params[:search]. In the code I mentioned above gives priority to search and then to page, but you get the idea, right?
Now, let's add user specific params method in the controller:
def user_params
params.slice(:brand, :page, :state).merge!({sort_column: sort_column, sort_direction: sort_direction, per_page: per_page })
end
However, in Rails 4 it is easier to do with strong parameters, e.g.: params.require(:user).permit(:search,..) etc.
Now, your controller's index method can look something like this:
def index
if params[:search].present?
#user = User.find_by_client_code(params[:search])
redirect_to edit_user_path(#user)
else
#users = User.fetch_with_brand(user_params)
end
end
Or you can refactor it further if you tend to redirect user to edit page in more places:
before_filter :redirect_to_edit, only: [:index, :some_other_method_name]
def index
#users = User.fetch_with_brand(user_params)
end
def redirect_to_edit
if params[:search].present?
#user = User.find_by_client_code(params[:search])
redirect_to edit_user_path(#user)
end
end
You have your skinny controller up now.

Related

Rails 4: how to include selected association in searching/filtering a model entity

First of all, I am totally new to Rails and still on the steep learning curve. I recently took over a rails project and need to do some tweaking on the existing code.
I need to include an association named 'School' when searching/filtering the model named 'Teacher'. A teacher has_many schools.
In the search method of the teacher controller, I have the following:
def search
if params[:id].present? || params[:city].present?
#teachers = Teacher.include(:schools).all
#teachers = #teachers.matches('id', params[:id]) if params[:id].present?
#teachers = #teachers.matches('name', params[:city]) if params[:city].present?
else
#teachers = []
end
end
However, the .include(:schools) does not work for me. I also tried to use .eager_loading(:schools), but does not work either.
Maybe try something like:
def search
if params[:id].present? || params[:city].present?
#teachers = Teacher.includes(:schools)
if params[:id].present?
#teachers = #teachers.find params[:id]
elsif params[:city].present?
#teachers = #teachers.find_by_name params[:city]
end
else
#teachers = []
end
end
I think you are using include instead of includes. See here.

Rails if conditional in controller error

I'm wondering how can I print on the index of my project only the rooms with the :is_available column or the rooms table with the :true value (is boolean).
I can't figure out how to achieve this (Sorry but I'm new with Rails). Any advice will be very appreciate!
I've this error with my current code:
"ActiveRecord::RecordNotFound in RoomsController#home
Couldn't find Room without an ID"
Here is my rooms_controller code:
class RoomsController < ApplicationController
before_action :get_room, only: [:index, :home]
def index
end
def show
#room = Room.find(params[:id])
end
def home
if params[:set_locale]
redirect_to root_url(locale: params[:set_locale])
else
puts #rooms if Room.all(params[:is_available => :true])
end
end
def get_room
#rooms = Room.all
end
end
You already have got #rooms = Room.all, you just need to precise your query (from all to your is_available restriction).
def home
if params[:set_locale]
redirect_to root_url(locale: params[:set_locale])
else
puts #rooms.where(is_available: true)
end
end
Also, you should avoid using puts in your controller logic. Either pass variable to the view (you can change #rooms value or create new variable #available_rooms), respond_with it or log it using Rails.logger if you use puts as a debugging solution.
def index
end
def home
if params[:set_locale]
redirect_to root_url(locale: params[:set_locale])
elsif params[:is_available]
puts #rooms
end
end
def get_room
#rooms = Room.where(is_available: true)
end
Using puts in controller - not a good idea.Use view to show the data.
There are several issues you may have:
Routes
Your index method looks empty. I presume you're using "home" as a substitute
In this case, you have to know what type of action this is - a member or collection action? The reason this is important is that when you define your routes, you have to ensure you define the route in the right way. For your home route, I'd have done this:
#config/routes.rb
resources :rooms do
get "home", action: "home"
end
Scopes
You can use a scope to bring back all the values with :is_available present. This lives in the model like this:
#app/models/room.rb
Class Room < ActiveRecord::Base
scope :is_available?, -> { where(is_available: true) }
end
This will allow you to call
#room = Room.is_available?
Code
Although you've not given us any context of the error (when it happens, what you do to make it happen), this is what I would do to help fix it:
#app/controllers/rooms_controller.rb
def home
if params[:set_locale]
redirect_to root_url(locale: params[:set_locale])
else
puts Room.is_available?
end
end
This may change depending the params you send & how you send them
def home
if params[:set_locale]
redirect_to root_url(locale: params[:set_locale])
else
puts #rooms if params[:is_available] && Room.where(is_available: true)
end
end
should work.

Rails: Displaying published post by all and unpublished post of current_user

I wanted to know how one would do the following:
A user can view all published Posts
A user can view view their unpublished Post
Code:
# Post model
scope :published, where(is_published: true)
scope :unpublished, where(is_published: false)
# Post controller
def index
#Post = Post.published
if user_signed_in?
#Post = Post.published && Post.unpublished.where(user_id: current_user.id)
end
end
I'm not really sure what the right way to setup an active record condition to display what I'm after.
Any much is much appreciated.
You're pretty close! Just replace && with +
# Post controller
def index
#posts = Post.published
if user_signed_in?
#posts = Post.published + Post.unpublished.where(user_id: current_user.id)
end
end
Be aware that joining like this will change the #posts object from a relation to an array.
Also take a look at #SachinR's answer for a nice refinement to the Post.unpublished.where(user_id: current_user.id) line.
Based on your requirement I think you could do better with a scope:
#Post model
scope :published_and_user, lambda{|user| where("is_published = ? OR user_id = ?", true, user.id)}
scope :ordered, :order => "created_at DESC"
# Post controller
def index
#posts = Post.published.ordered
if user_signed_in?
#posts = Post.published_and_user(current_user).ordered
end
end
And now you have a relation that is ordered properly, and only one scope!
To get all published records
#posts = Post.where("user_id = ?", current_user.id).published
To get all unpublished records
#posts = Post.where("user_id = ?", current_user.id).unpublished
or
If Post belongs to user
class Post
belongs_to :user
end
then you can directly use
current_user.posts.published
current_user.posts.unpublished

Is there a more idiomatic way to handle this controller logic in Rails?

def index
#workouts = Workout.all
#user_workouts = current_user.workouts.order("created_at DESC") unless current_user.blank?
if #client.present?
#user_workouts = #client.workouts.order("created_at DESC")
end
respond_to do |format|
format.html # index.html.erb
format.json { render json: #workouts }
end
end
The issue in question here is the instance variable #user_workouts - I am using a nested route to be able to do something like /clients/1/workouts instead of /workouts which will show the current users workouts which if that were nested would be /users/1/workouts.
Any idiomatic way to handle this or is it normal to just let the conditionals creep in?
You could create a class method on Workout accepting a User instance
def self.for_user(user)
where(user_id: user.id).order("created_at DESC")
end
and then simplify your action
def index
#workouts = Workouts.all
#user_workouts = Workout.for_user(#client || current_user)
respond_to ...
If #client exists it will be passed to for_user, otherwise current_user will.
You could DRY it up a little with:
user = #client || current_user
#user_workouts = user.workouts.order("created_at DESC")
Other than that, it looks pretty good as-is.

Issue while using || (OR) in if statement

In my project controller I have the actions below. As you can see 'check_if_owner_or_member' is called when the 'show' view is rendered, which checks whether or not a user is a member of the project or administrator. If that's not the case the user get's an error message and gets redirected to the root.
When trying out the action it works if the user is admin, but not if the user is a member. So, something is apparently wrong with 'if !is_owner || !is_member', because it works if I only try with 'if !is_member'.
What am I doing wrong?
before_filter :check_if_owner_or_member, :only => [:show]
def is_owner
Project.where("id = ? AND user_id = ?", params[:id], current_user.id).count > 0
end
def is_member
ProjectsUser.where("project_id = ? AND user_id = ?", params[:id], current_user.id).count > 0
end
def check_if_owner_or_member
if !is_owner || !is_member
redirect_to root_path
flash[:error] = "You don't have permission to the project!"
end
end
You should refactor your code like this:
before_filter :check_if_owner_or_member, :only => [:show]
def is_owner?
Project.exists?(id: params[:id], user_id: current_user.id)
end
def is_member?
ProjectsUser.exists?(project_id: params[:id], user_id: current_user.id)
end
def check_if_owner_or_member
unless is_owner? || is_member? # as TheDude said, you probably meant && here
redirect_to root_path
flash[:error] = "You don't have permission to the project!"
end
end
It is more readable and using the exists? method which is faster to execute than finding, counting and comparing to zero.
Since a member is not an admin, the first part would be true and the second part won't be executed. I imagine that you would want to use && here.

Resources