Refactoring a mongoid call - ruby-on-rails

Right now, i am using 2 different but very similar queries (difference is just an additional criteria
pop_answers = Answer.any_of(
{:num_likes.gte=>3, :image_filename.exists=>true},
).desc(:created_at).skip(to_skip).limit(per_page).map{|a|a}
pop_answers_in_topic = Answer.any_of(
{:num_likes.gte=>3, :image_filename.exists=>true, :topic_id=>some_id},
).desc(:created_at).skip(to_skip).limit(per_page).map{|a|a}
How can i refactor this?

You could add a class method to Answer:
def self.popular(offset, limit, for_topic_id = nil)
conditions = { :num_likes.gte => 3, :image_filename.exists => true }
conditions[:topic_id] = for_topic_id if(for_topic_id)
any_of(conditions).desc(:created_at).skip(offset).limit(limit).map{|a|a}
end
Or if you're expecting more than just a topic ID:
def self.popular(offset, limit, options = { })
conditions = { :num_likes.gte => 3, :image_filename.exists => true }.merge(options)
any_of(conditions).desc(:created_at).skip(offset).limit(limit).map{|a|a}
end
I don't use Mongoid but you might be able to drop the funny .map{|a|a} or use .to_a instead.
Or perhaps something scope-ish:
# In answer.rb
def self.popular(for_topic_id = nil)
conditions = { :num_likes.gte => 3, :image_filename.exists => true }
conditions[:topic_id] = for_topic_id if(for_topic_id)
any_of(conditions)
end
# And then where you're using it...
pop_answers = Answer.popular.desc(:created_at).skip(to_skip).limit(per_page).map{|a|a}
pop_in_topic = Answer.popular(some_id).desc(:created).skip(to_skip).limit(per_page).map{|a|a}
And I have to wonder if any_of is really what you're looking for here, perhaps all_of would make more sense.

Related

how append an object to association relation in rails?

In a rails 4.1 application I need to add an object to an "AssociationRelation"
def index
employee = Employee.where(id_person: params[:id_person]).take
receipts_t = employee.receipts.where(:consent => true) #gives 3 results
receipts_n = employee.receipts.where(:consent => nil).limit(1) #gives 1 result
#I would need to add the null consent query result to the true consent results
#something similar to this and the result is still an association relation
#receipts = receipts_t + receipts_n
end
Is there a simple way to do this?
A way of solving this:
def index
employee_receipts = Employee.find_by(id_person: params[:id_person]).receipts
receipts_t = employee_receipts.where(consent: true)
receipts_n = employee_receipts.where(consent: nil).limit(1)
#receipts = Receipt.where(id: receipts_t.ids + receipts_n.ids)
end
Unfortunately .or() can't be used here because it's only available from Rails v5.0.0.1
you could do this way
receipts_t_ids = employee.receipts.where(:consent => true).pluck(:id)
receipts_n_ids = employee.receipts.where(:consent => nil).limit(1).pluck(:id)
#receipts = Receipt.where(id: receipts_t_ids + receipts_n_ids)
To avoid extra queries and keeping arrays in memory, you can use or
Like this:
def index
employee_receipts = Employee.find_by(id_person: params[:id_person]).receipts
#receipts =
employee_receipts.where(consent: true).or(
employee_receipts.where(consent: nil).limit(1)
)
end

Generic method to set attributes

In my model I have attributes: is_a, is_b and is_c. By default all are null.
I need APIs to set them. These attributes can be set as strictly one or in group. If I am to write APIs, I will be doing following in my model:
def set_as_a # strictly a
self.update_attributes!(:is_a => true, :is_b => false, :is_c => false)
end
def set_as_b # strictly b
self.update_attributes!(:is_a => false, :is_b => true, :is_c => false)
end
... # strictly c
def set_as_a_and_b # a and b
self.update_attributes!(:is_a => true, :is_b => true, :is_c => false)
end
..... # so on
While this works, it does not look elegant. Also if in future if the set has more than 3 attributes, it will result more repetitive code. What is the correct elegant way to achieve this?
class SettableAsABC
ATTRS = [:a, :b, :c]
METHOD_RE = /^set_as_([[:alnum:]]+?(?:_and_[[:alnum:]]+?)*)$/
def method_missing(name, *args)
if name.to_s =~ METHOD_RE
trues = $1.split('_and_').map(&:to_sym)
attrs = Hash[ATTRS.map { |a| ["is_#{a}".to_sym, trues.include?(a)] }]
update_attributes(attrs)
else
super
end
end
def respond_to_missing?(name, include_private = false)
!!(name =~ METHOD_RE) || super
end
end
a = SettableAsABC.new
a.set_as_a_and_c
No defining 2^N methods, just plain Ruby metaprogramming.
EDIT: Good point, #Stefan.
EDIT2: My previous edit introduced a bug. Fixed now.
EDIT3: TIL about respond_to_missing?
I might be misunderstanding something, but why not just write a single method that takes params?:
def set_attributes(opts = {})
update_attributes!(opts) unless opts.none?
end
# usage
set_attributes(is_a: false, is_b: true)
EDIT
To dynamically create methods for combinations of your attributes here is what I came up with:
attributes = %w(a b c d)
(1..attributes.size).flat_map { |size| attributes.combination(size).to_a }.each do |methods|
define_method "set_as_#{methods.join('_and_')}" do
update_attributes!(Hash[methods.map { |v| ["is_#{v}", true] }])
end
end
It will generate the following menthods:
set_as_a
set_as_b
set_as_c
set_as_d
set_as_a_and_b
set_as_a_and_c
set_as_a_and_d
set_as_b_and_c
set_as_b_and_d
set_as_c_and_d
set_as_a_and_b_and_c
set_as_a_and_b_and_d
set_as_a_and_c_and_d
set_as_b_and_c_and_d
set_as_a_and_b_and_c_and_d
How about this?
def set_true(true_fields=[])
attr_hash = {}
true_fields.each { |field| attr_hash[field] = true }
update_attributes(hash)
end
Hope that helps!

Create or Update Rails 4 - updates but also creates (Refactoring)

In my Rails API I have the following code in my Child model:
before_create :delete_error_from_values, :check_errors, :update_child_if_exists
def delete_error_from_values
#new_error = self.values["error"]
#values = self.values.tap { |hs| hs.delete("error") }
end
def update_child_if_exists
conditions = {type: self.type, parent_id: self.parent_id}
if existing_child = Child.find_by(conditions)
new_values = existing_child.values.reverse_merge!(#values)
hash = {:values => new_values}
existing_child.update_attributes(hash)
end
end
def check_errors
if self.type == "error"
conditions = {type: self.type, parent_id: self.parent_id}
if existing_child = Child.find_by(conditions)
bd_errors = existing_child.error_summary
bd_errors[#new_error] = bd_errors[#new_error].to_i + 1
hash = {:error_summary => bd_errors}
existing_child.update_attributes(hash)
else
self.error_summary = {#new_error => 1}
end
end
end
This works like expected, except for one small detail: The Child is updated if a record by type and parent_id already exists, but it is also created. How can I refactor this to stop creation?
I've tried to include return false, but if I do this, the update is not successful.
I wish to have something like find_or_create_by, but I'm not sure how to use it for this cases.
May be you can refactor your code in following approach:
def create
#parent = Parent.find(params[:parent_id])
existing_child = Child.where(type: child_params[:type], parent_id:
child_params[:parent_id]).first
if existing_child.present?
existing_child.update_attributes(attribute: value_1)
else
#child = #parent.child.build(child_params)
end
#other saving related code goes here.
end
This is just a basic piece of example.
Try creating separate instance methods to keep the Contrller DRY. :)

How to refactor complicated logic in create_unique method?

I would like to simplify this complicated logic for creating unique Track object.
def self.create_unique(p)
f = Track.find :first, :conditions => ['user_id = ? AND target_id = ? AND target_type = ?', p[:user_id], p[:target_id], p[:target_type]]
x = ((p[:target_type] == 'User') and (p[:user_id] == p[:target_id]))
Track.create(p) if (!f and !x)
end
Here's a rewrite of with a few simple extract methods:
def self.create_unique(attributes)
return if exists_for_user_and_target?(attributes)
return if user_is_target?(attributes)
create(attributes)
end
def self.exists_for_user_and_target?(attributes)
exists?(attributes.slice(:user_id, :target_id, :target_type))
end
def self.user_is_target?(attributes)
attributes[:target_type] == 'User' && attributes[:user_id] == attributes[:target_id]
end
This rewrite shows my preference for small, descriptive methods to help explain intent. I also like using guard clauses in cases like create_unique; the happy path is revealed in the last line (create(attributes)), but the guards clearly describe exceptional cases. I believe my use of exists? in exists_for_user_and_target? could be a good replacement for find :first, though it assumes Rails 3.
You could also consider using uniqueness active model validation instead.
##keys = [:user_id, :target_id, :target_type]
def self.create_unique(p)
return if Track.find :first, :conditions => [
##keys.map{|k| "#{k} = ?"}.join(" and "),
*##keys.map{|k| p[k]}
]
return if p[##keys[0]] == p[##keys[1]]
return if p[##keys[2]] == "User"
Track.create(p)
end

Rails: How to manipulate find(params[])

I am trying to use Object.find(params[]) to only return objects with :stage_id = integer
Here is my controller code
def show
#lesson = Lesson.find(params[:id])
#stage1 = #lesson.units(params[:stage_id] == 1)
#stage2 = #lesson.units(params[:stage_id] == 2)
Each lesson has many units, each unit has either a stage_id = 1 or stage_id = 2, I want #stage1 to become an array with units that only have a stage_id value of 1. The same goes for stage2.
How can I properly use params to return only units that have the indicated table values?
def show
#lesson = Lesson.find(params[:id])
#stage1 = #lesson.units.first(:conditions => { :stage_id => 1 })
#stage2 = #lesson.units.first(:conditions => { :stage_id => 2 })
end
Ref find
#stage1 = Unit.find(:all, :conditions => ["stage_id=? AND lession_id=?" 1, params[:id]])
#stage2 = Unit.find(:all, :conditions => ["stage_id=? AND lession_id=?" 2, params[:id]])
If Units are "always' going to be structured with Stages, one thing you could do to DRY up your code is to have a Stage model. That allows flexibility to add more stages in the future without breaking code. Assuming that relationship is properly established and data is good, you could do something like the following.
controller code
#lesson = Lesson.find(params[:id])
view code (haml)
- for stage in #lesson.stages
= stage.name
- for unit in stage.units
= unit.name

Resources