How to refactor this Ruby on Rails code? - ruby-on-rails

I want to fetch posts based on their status, so I have this code inside my PostsController index action. It seems to be cluttering the index action, though, and I'm not sure it belongs here.
How could I make it more concise and where would I move it in my application so it doesn't clutter up my index action (if that is the correct thing to do)?
if params[:status].empty?
status = 'active'
else
status = ['active', 'deleted', 'commented'].include?(params[:status]) ? params[:status] : 'active'
end
case status
when 'active'
#active posts are not marked as deleted and have no comments
is_deleted = false
comments_count_sign = "="
when 'deleted'
#deleted posts are marked as deleted and have no comments
is_deleted = true
comments_count_sign = "="
when 'commented'
#commented posts are not marked as deleted and do have comments
is_deleted = false
comments_count_sign = ">"
end
#posts = Post.find(:all, :conditions => ["is_deleted = ? and comments_count_sign #{comments_count_sign} 0", is_deleted])

class Post < ActiveRecord::Base
named_scope :active, :conditions => { :is_deleted => false, :emails_count => 0 }
named_scope :sent, :conditions => ["is_deleted = ? AND emails_count > 0", true]
...
end
use it like Post.active.all, Post.active.first, Post.active.each, etc
and then
status = %w'active deleted sent'.include?(params[:status]) : params[:status] : 'active'
#posts = Post.send(status).all

I would consider adding a class method to Post
def file_all_based_on_status status
# custom logic for queries based on the given status here
# handling nils and other cases
end
That way your Controller index is simple
def index
#posts = Post.find_all_based_on_status params[:status]
end

Related

Rails 5 and ActiveRecord: reusable filters for listing resources

In my application I have a number of pages where I need to display a list of people and allow the user to filter them with a form. And these pages are often similar looking. The filters share parts but still not the same.
I'm wondering how can I avoid repeating almost the same code for different controllers? I tried scopes but I still need to parse parameters and populate form in a view anyway.
Thanks!
Disclaimer: author of https://github.com/dubadub/filtered is here.
ActiveRecord offers a merge method for relations. It intersects two query parts which allows breaking query logic into parts.
Based on that idea I created a gem https://github.com/dubadub/filtered.
In your case it could be something like:
# app/controllers/people_controller.rb
class PeopleController < ApplicationController
before_action :set_filter
def index
#people = People.all.merge(#filter)
end
private
def set_filter
#filter = PersonFilter.new(filter_params)
end
def filter_params
params.fetch(:filter, {}).permit(:age, :active, :sorting)
end
end
# app/filters/person_filter.rb
class PersonFilter < ApplicationFilter
field :age
field :active do |active|
-> { joins(:memberships).merge(Membership.where(active: active)) }
end
field :sorting do |value|
order_by, direction = value.values_at("order", "direction")
case order_by
when "name"
-> { order(name: direction) }
when "age"
-> { order(age: direction) }
else
raise "Incorrect Filter Value"
end
end
end
# app/views/people/index.slim
= form_for(#filter, url: search_path, method: "GET", as: :filter) do |f|
.fields
span Age
= f.select :age, (18..90).map { |a| [ a, a ] }
.fields
span Active
= f.check_box :active
.fields
span Sorting
span Name
= f.radio_button :sorting, "name asc"
= f.radio_button :sorting, "name desc"
span Age
= f.radio_button :sorting, "age asc"
= f.radio_button :sorting, "age desc"
.actions
= f.submit "Filter"
Hope it helps!
Have you had a look at query objects?
https://mkdev.me/en/posts/how-to-use-query-objects-to-refactor-rails-sql-queries
They allow you to reuse the code in many places, you'd be able to simply pass the params.permit(...) and get get AR output.
# app/queries/user_query.rb
class UserQuery
attr_accessor :initial_scope
def initialize(scoped = User.all)
#initial_scope = initial_scope
end
def call(params) # is what you pass from your controller
scoped = by_email(#initial_scope, params[:email]
scoped = by_phone(scoped, params[:phone]
# ...
scoped
end
def by_email(scoped, email = nil)
email ? where(email: email) : scoped
end
def by_phone(scoped, phone = nil)
phone ? where(phone: phone) : scoped
end
end
# users_controller.rb
class UsersController < ApplicationController
def index
#users = UserQuery.new(User.all)
.call(params.permit(:email, :phone))
.order(id: :desc)
.limit(100)
end
end
# some other controller
class RandomController < ApplicationController
def index
#users = UserQuery.new(User.where(status: 1))
.call(params.permit(:email))
.limit(1)
end
end
You can probably refactor this example to reduce the upfront investment into writing these queries for richer objects, do post here if you come up with alternatives for so that others can learn how to use query objects.

Rails- Building dynamic query for filtering search results

I am trying to build a dynamic querying method to filter search results.
My models:
class Order < ActiveRecord::Base
scope :by_state, -> (state) { joins(:states).where("states.id = ?", state) }
scope :by_counsel, -> (counsel) { where("counsel_id = ?", counsel) }
scope :by_sales_rep, -> (sales) { where("sales_id = ?", sales) }
scope :by_year, -> (year) { where("title_number LIKE ?", "%NYN#{year}%") }
has_many :properties, :dependent => :destroy
has_many :documents, :dependent => :destroy
has_many :participants, :dependent => :destroy
has_many :states, through: :properties
belongs_to :action
belongs_to :role
belongs_to :type
belongs_to :sales, :class_name => 'Member'
belongs_to :counsel, :class_name => 'Member'
belongs_to :deal_name
end
class Property < ActiveRecord::Base
belongs_to :order
belongs_to :state
end
class State < ActiveRecord::Base
has_many :properties
has_many :orders, through: :properties
end
I have a page where I display ALL orders by default. I want to have check boxes to allow for filtering of the results. The filters are: Year, State, Sales, and Counsel. an example of a query is: All orders in 2016, 2015("order.title_number LIKE ?", "%NYN#{year}%") in states (has_many through) NJ, PA, CA, etc with sales_id unlimited ids and counsel_id unlimited counsel_ids.
In a nut shell I am trying to figure out how to create ONE query that takes into account ALL options the user checks. Here is my current query code:
def Order.query(opt = {})
results = []
orders = []
if !opt["state"].empty?
opt["state"].each do |value|
if orders.empty?
orders = Order.send("by_state", value)
else
orders << Order.send("by_state", value)
end
end
orders = orders.flatten
end
if !opt["year"].empty?
new_orders = []
opt["year"].each do |y|
new_orders = orders.by_year(y)
results << new_orders
end
end
if !opt["sales_id"].empty?
end
if !opt["counsel_id"].empty?
end
if !results.empty?
results.flatten
else
orders.flatten
end
end
Here is the solution I have come up with to allow for unlimited amount of filtering.
def self.query(opts = {})
orders = Order.all
opts.delete_if { |key, value| value.blank? }
const_query = ""
state_query = nil
counsel_query = nil
sales_query = nil
year_query = nil
queries = []
if opts["by_year"]
year_query = opts["by_year"].map do |val|
" title_number LIKE '%NYN#{val}%' "
end.join(" or ")
queries << year_query
end
if opts["by_sales_rep"]
sales_query = opts["by_sales_rep"].map do |val|
" sales_id = '#{val}' "
end.join(" or ")
queries << sales_query
end
if opts["by_counsel"]
counsel_query = opts["by_counsel"].map do |val|
" counsel_id = '#{val}' "
end.join(" or ")
queries << counsel_query
end
if opts["by_state"]
state_query = opts["by_state"].map do |val|
"states.id = '#{val}'"
end.join(" or ")
end
query_string = queries.join(" AND ")
if state_query
#orders = Order.joins(:states).where("#{state_query}")
#orders = #orders.where(query_string)
else
#orders = orders.where("#{query_string}")
end
#orders.order("title_number DESC")
end
What you're looking for a query/filter object, which is a common pattern. I wrote an answer similar to this, but I'll try to extract the important parts.
First you should move those logic to it's own object. When the search/filter object is initialized it should start with a relation query (Order.all or some base query) and then filter that as you go.
Here is a super basic example that isn't fleshed out but should get you on the right track. You would call it like so, orders = OrderQuery.call(params).
# /app/services/order_query.rb
class OrderQuery
def call(opts)
new(opts).results
end
private
attr_reader :opts, :orders
def new(opts={})
#opts = opts
#orders = Order.all # If using Rails 3 you'll need to use something like
# Order.where(1=1) to get a Relation instead of an Array.
end
def results
if !opt['state'].empty?
opt['state'].each do |state|
#orders = orders.by_state(state)
end
end
if !opt['year'].empty?
opt['year'].each do |year|
#orders = orders.by_year(year)
end
end
# ... all filtering logic
# you could also put this in private functions for each
# type of filter you support.
orders
end
end
EDIT: Using OR logic instead of AND logic
# /app/services/order_query.rb
class OrderQuery
def call(opts)
new(opts).results
end
private
attr_reader :opts, :orders
def new(opts={})
#opts = opts
#orders = Order.all # If using Rails 3 you'll need to use something like
# Order.where(1=1) to get a Relation instead of an Array.
end
def results
if !opt['state'].empty?
#orders = orders.where(state: opt['state'])
end
if !opt['year'].empty?
#orders = orders.where(year: opt['year'])
end
# ... all filtering logic
# you could also put this in private functions for each
# type of filter you support.
orders
end
end
The above syntax basically filters sayings if state is in this array of states and year is within this array of years.
In my case, the filter options came from the Controller's params, so I've done something like this:
The ActionController::Parameters structure:
{
all: <Can be true or false>,
has_planned_tasks: <Can be true or false>
... future filters params
}
The filter method:
def self.filter(filter_params)
filter_params.reduce(all) do |queries, filter_pair|
filter_key = filter_pair[0]
filter_value = filter_pair[1]
return {
all: proc { queries.where(deleted_at: nil) if filter_value == false },
has_planned_tasks: proc { queries.joins(:planned_tasks).distinct if filter_value == true },
}.fetch(filter_key).call || queries
end
end
Then I call the ModelName.filter(filter_params.to_h) in the Controller. I was able to add more conditional filters easily doing like this.
There's space for improving here, like extract the filters logic or the whole filter object, but I let you decide what is better in your context.
Here is one I built for an ecommerce order dashboard in Rails with the parameters coming from the controller.
This query will execute twice, once to count the orders and once to return the requested orders according to the parameters in the request.
This query supports:
Sort by column
Sort direction
Incremental Search - It'll search the beginning of a given field and returns those records that match enabling real-time suggestions while searching
Pagination (limited by 100 records per page)
I also have predefined values to sanitize some of the data.
This style is extremely clean and easy for others to read and modify.
Here's a sample query:
api/shipping/orders?pageNumber=1&orderStatus=unprocessedOrders&filters=standard,second_day&stores=82891&sort_column=Date&sort_direction=dsc&search_query=916
And here's the controller code:
user_id = session_user.id
order_status = params[:orderStatus]
status = {
"unprocessedOrders" => ["0", "1", "4", "5"],
"processedOrders" => ["2", "3", "6"],
"printedOrders" => ["3"],
"ratedOrders" => ["1"],
}
services = [
"standard",
"expedited",
"next_day",
"second_day"
]
countries = [
"domestic",
"international"
]
country_defs = {
domestic: ['US'],
international: ['CA', 'AE', 'EU', 'GB', 'MX', 'FR']
}
columns = {
Number: "order_number",
QTY: "order_qty",
Weight: "weight",
Status: "order_status",
Date: "order_date",
Carrier: "ship_with_carrier",
Service: "ship_with_carrier_code",
Shipping: "requestedShippingService",
Rate: "cheapest_rate",
Domestic: "country",
Batch: "print_batch_id",
Skus: "skus"
}
# sort_column=${sortColumn}&sort_direction=${sortDirection}&search_query=${searchQuery}
filters = params[:filters].split(',')
stores = params[:stores].split(',')
sort_column = params[:sort_column]
sort_direction = params[:sort_direction]
search_query = params[:search_query]
sort_by_column = columns[params[:sort_column].to_sym]
sort_direction = params[:sort_direction] == "asc" ? "asc" : "desc"
service_params = filters.select{ |p| services.include?(p) }
country_params = filters.select{ |p| countries.include?(p) }
order_status_params = filters.select{ |p| status[p] != nil }
query_countries = []
query_countries << country_defs[:"#{country_params[0]}"] if country_params[0]
query_countries << country_defs[:"#{country_params[1]}"] if country_params[1]
active_filters = [service_params, country_params].flatten
query = Order.where(user_id: user_id)
query = query.where(order_status: status[order_status]) if order_status_params.empty?
query = query.where("order_number ILIKE ? OR order_id::TEXT ILIKE ? OR order_info->'advancedOptions'->>'customField2' ILIKE ?", "%#{search_query}%", "%#{search_query}%", "%#{search_query}%") unless search_query.gsub(/\s+/, "").length == 0
query = query.where(requestedShippingService: service_params) unless service_params.empty?
query = query.where(country: "US") if country_params.include?("domestic") && !country_params.include?("international")
query = query.where.not(country: "US") if country_params.include?("international") && !country_params.include?("domestic")
query = query.where(order_status: status[order_status_params[0]]) unless order_status_params.empty?
query = query.where(store_id: stores) unless stores.empty?\
order_count = query.count
num_of_pages = (order_count.to_f / 100).ceil()
requested_page = params[:pageNumber].to_i
formatted_number = (requested_page.to_s + "00").to_i
query = query.offset(formatted_number - 100) unless requested_page == 1
query = query.limit(100)
query = query.order("#{sort_by_column}": :"#{sort_direction}") unless sort_by_column == "skus"
query = query.order("skus[1] #{sort_direction}") if sort_by_column == "skus"
query = query.order(order_number: :"#{sort_direction}")
orders = query.all
puts "After querying orders mem:" + mem.mb.to_s
requested_page = requested_page <= num_of_pages ? requested_page : 1
options = {}
options[:meta] = {
page_number: requested_page,
pages: num_of_pages,
type: order_status,
count: order_count,
active_filters: active_filters
}
render json: OrderSerializer.new(orders, options).serialized_json

Spree search filter for properties and variants

I'm running Spree 1.3.1 and I'm trying to customize the Taxon show page.
I would like it to return the products contained inside the current Taxon, eventually filtered by a property or by an option value.
For example let's say that I'm seeing the Taxon of an underwear collection.
I'd like to filter the products shown, by providing a certain size (option_type).
In this case I should list only products that have variants with the requested size.
I would like also to be able to filter the products by the "fit" property.
Filtering by the slip fit, I should be able to list only products inside the current Taxon that have the required property.
This is the Taxon controller show action:
Spree::TaxonsController.class_eval do
def show
#taxon = Spree::Taxon.find_by_permalink!(params[:id])
return unless #taxon
#searcher = Spree::Config.searcher_class.new(params)
#searcher.current_user = try_spree_current_user
#searcher.current_currency = current_currency
#products = #searcher.retrieve_products
respond_with(#taxon)
end
end
How should I modify it to fit my needs?
I partially solved the question.
I found out that I need to leave the controller as it is, the magic is done in the lib/spree/product_filters.rb file where I added this new product filter:
if Spree::Property.table_exists?
Spree::Product.add_search_scope :fit_any do |*opts|
conds = opts.map {|o| ProductFilters.fit_filter[:conds][o]}.reject {|c| c.nil?}
scope = conds.shift
conds.each do |new_scope|
scope = scope.or(new_scope)
end
Spree::Product.with_property("fit").where(scope)
end
def ProductFilters.fit_filter
fit_property = Spree::Property.find_by_name("fit")
fits = Spree::ProductProperty.where(:property_id => fit_property).pluck(:value).uniq
pp = Spree::ProductProperty.arel_table
conds = Hash[*fits.map { |b| [b, pp[:value].eq(b)] }.flatten]
{ :name => "Fits",
:scope => :fit_any,
:conds => conds,
:labels => (fits.sort).map { |k| [k, k] }
}
end
end
Then I added the new filter to the Taxon model decorator with this:
Spree::Taxon.class_eval do
def applicable_filters
fs = []
fs << Spree::Core::ProductFilters.fit_filter if Spree::Core::ProductFilters.respond_to?(:fit_filter)
fs
end
end
Still I haven't found out how to create a filter for variants that have a specific option value.
You talk about filtering on numerical value, I've written a filter for option ranges:
def ProductFilters.ov_range_test(range1, range2)
ov = Arel::Table.new("spree_option_values")
cast = Arel::Nodes::NamedFunction.new "CAST", [ ov[:presentation].as("integer")]
comparaisons = cast.in(range1..range2)
comparaisons
end
Spree::Product.add_search_scope :screenSize_range_any do |*opts|
conds = opts.map {|o| Spree::ProductFilters.screenSize_filter[:conds][o]}.reject {|c| c.nil?}
scope = conds.shift
conds.each do |new_scope|
scope = scope.or(new_scope)
end
option_values=Spree::OptionValue.where(scope).joins(:option_type).where(OptionType.table_name => {:name => "tailleEcran"}).pluck("#{OptionValue.table_name}.id")
Spree::Product.where("#{Product.table_name}.id in (select product_id from #{Variant.table_name} v left join spree_option_values_variants ov on ov.variant_id = v.id where ov.option_value_id in (?))", option_values)
end
def ProductFilters.screenSize_filter
conds = [ [ "20p ou moins",ov_range_test(0,20)],
[ "20p - 30p",ov_range_test(20,30)],
[ "30p - 40p" ,ov_range_test(30,40)],
[ "40p ou plus",ov_range_test(40,190)]]
{ :name => "taille",
:scope => :screenSize_range_any,
:options => :tailleEcran,
:conds => Hash[*conds.flatten],
:labels => conds.map {|k,v| [k,k]}
}
end
You can see this one too, for discrete specific values:
https://gist.github.com/Ranger-X/2511088

Can't delete a embedded element with Mongoid

Still working on my Rails/MongoDB app, I have yet another problem.
This time, I can create embedded documents, but can't delete them, though I've been doing what has been said in another Stackoverflow topic (http://stackoverflow.com/questions/3693842/remove-an-embedded-document-in-mongoid)
Here goes my controller :
class FeedSubscribtionsController < ApplicationController
has_to_be_connected
def create
if session[:user_id] != params[:id]
#self = current_user
attributes = { :user => #self, :userId => params[:id], :feedId => params[:feed] }
subscribtion = FeedSubscribtion.create attributes
success = subscribtion.save
render json: { :success => success, :feed => params[:feed] }
end
end
def destroy
success = false
if session[:user_id] != params[:id]
#self = current_user
uid, fid = params[:id], params[:feed]
#feed = #self.feed_subscribtions.where :userId => uid, :feedId => fid
if #feed.count > 0
#self.feed_subscribtions.delete #feed.first.id.to_s
success = #feed.first.save
end
end
render json: { :success => success, :feed => params[:feed] }
end
end
The weirdest part is that everything seems to go well : success is equal to true in the rendered JSON object.
I also tried to replace "success = #feed.first.save" with "#self.save" : in that case, it returns false, but with no further explanations.
(I do know that for the logic behind this controller to be perfect, I should loop on the #feed array, and I will once it starts working ^^ it's just easier to debug that way)
So, is there any way I may find out why #ßelf.save fails, or why #feed.first.save doesn't fail but doesn't actually save either ?
Thanks.
Here is what you're doing as I see it
You are using FeedSubscriptionsController to delete the object with id if #feed.first, then you try and save #feed.first, but #feed.first points to an already deleted object, so it's failing to save it.

Making a fat controller in rails 3 skinny

I have this terribly large controller in my app. I'd really like to make it as skinny as possible. Below is some of the code, showing the types of things I'm currently doing.. I'm wondering what things I can move out of this?
A note - this is not my exact code, a lot of it is similar. Essentially every instance variable is used in the views - which is why I dont understand how to put the logic in the models? Can models return the values for instance variables?
def mine
#For Pusher
#push_ch = "#{current_user.company.id}"+"#{current_user.id}"+"#{current_user.profile.id}"
#Creating a limit for how many items to show on the page
#limit = 10
if params[:limit].to_i >= 10
#limit = #limit + params[:limit].to_i
end
#Setting page location
#ploc="mine"
#yourTeam = User.where(:company_id => current_user.company.id)
#Set the user from the param
if params[:user]
#selectedUser = #yourTeam.find_by_id(params[:user])
end
#Get all of the user tags
#tags = Tag.where(:user_id => current_user.id)
#Load the user's views
#views = View.where(:user_id => current_user.id)
if !params[:inbox]
#Hitting the DB just once for all the posts
#main_posts = Post.where(:company_id => current_user.company.id).includes(:status).includes(:views)
#main_posts.group_by(&:status).each do |status, posts|
if status.id == #status.id
if #posts_count == nil
#posts_count = posts
else
#posts_count = #posts_count + posts
end
elsif status.id == #status_act.id
if #posts_count == nil
#posts_count = posts
else
#posts_count = #posts_count + posts
end
end
end
if params[:status] == "All" || params[:status] == nil
#posts = Post.search(params[:search]).status_filter(params[:status]).user_filter(params[:user]).order(sort_column + " " + sort_direction).where(:company_id => current_user.company.id, :status_id => [#status.id, #status_act.id, #status_def.id, #status_dep.id, #status_up.id]).limit(#limit).includes(:views)
else
#posts = Post.search(params[:search]).status_filter(params[:status]).user_filter(params[:user]).order(sort_column + " " + sort_direction).where(:company_id => current_user.company.id).limit(#limit).includes(:views)
end
elsif params[:inbox] == "sent"
#yourcompanylist = User.where(:company_id => current_user.company.id).select(:id).map(&:id)
#yourcompany = []
#yourcompanylist.each do |user|
if user != current_user.id
#yourcompany=#yourcompany.concat([user])
end
end
if params[:t]=="all"
#posts = Post.search(params[:search]).status_filter(params[:status]).user_filter(params[:user]).tag_filter(params[:tag], current_user).order(sort_column + " " + sort_direction).where(:user_id => current_user.id).includes(:views, :tags).limit(#limit)
elsif params[:status]!="complete"
#posts = Post.search(params[:search]).status_filter(params[:status]).user_filter(params[:user]).tag_filter(params[:tag], current_user).order(sort_column + " " + sort_direction).where(:user_id => current_user.id).includes(:views, :tags).limit(#limit)
elsif params[:status]!=nil
#posts = Post.search(params[:search]).status_filter(params[:status]).user_filter(params[:user]).tag_filter(params[:tag], current_user).order(sort_column + " " + sort_direction).where(:user_id => current_user.id).includes(:views, :tags).limit(#limit)
end
end
respond_to do |format|
format.html # index.html.erb
format.js # index.html.erb
format.xml { render :xml => #posts }
end
end
You can start by moving logic into the model...
A line like this screams of feature envy:
#push_ch = "#{current_user.company.id}"+"#{current_user.id}"+"#{current_user.profile.id}"
I would recommend moving it into the model:
#user.rb
def to_pusher_identity
"#{self.company_id}#{self.id}#{self.profile_id}"
end
And then in your controller
#push_ch = current_user.to_pusher_identity
At this point you could even move this into a before_filter.
before_filter :supports_pusher, :only => :mine
Another thing you can do is create richer associations, so you can express:
#tags = Tag.where(:user_id => current_user.id)
as
#tags = current_user.tags
Another example would be for main posts, instead of
Post.where(:company_id => current_user.company.id).includes(:status).includes(:views)
you would go through the associations:
current_user.company.posts.includes(:status).includes(:views)
When I'm drying out a controller/action I try to identify what code could be (should be?) offloaded into the model or even a new module. I don't know enough about your application to really point to where these opportunities might lie, but that's where I'd start.
Few quick ideas:
Consider using respond_to/respond_with. This controller action can be splitted up to two separate ones - one for displaying #main_posts, another for params[:inbox] == "sent". The duplicate code can be removed using before_filters.
Also, a couple of gem suggestions:
use kaminari or will_paginate for pagination
meta_search for search and sorting

Resources