I have the following scope:
scope :user_reviews, lambda { |user| where(:user_id => user) }
I apply this in the controller:
def show
#review = #reviewable.reviews.user_reviews(current_user).first || Review.new
end
The first is to limit to search the current user's one and only review. Now I try to write a new scope user_review which I tried many ways to chain the user_reviews scope with first, but just couldn't get it what. Something like this:
scope :user_reviews, lambda { |user| where(:user_id => user) }
scope :user_review, lambda { |user| user_reviews(user).first }
I know the above user_review is wrong, but just trying to show you guys what I am trying to do.
How should I write this properly?
Thanks.
#Victor, just stick with your original idea. Use scope :user_reviews, lambda { |user| where(:user_id => user) } and call user_reviews.first. Nothing wrong with that.
Definitely do not define a scope that returns a single object. A scope should be chainable.
I also used this:
def self.user_review(user)
self.user_reviews(user).first
end
I feel this use case is very common.
We can solve this by following:
scope :user_reviews, lambda { | user_id | where(:user_id => user_id) }
and then in the model have user_review as class method
def self.user_review(user_id)
user_reviews(user_id).first
end
Now you can call user_review and it'll return the one record.
scope :user_reviews, ->(user) { where(user_id: :user) }
Related
I've got this in my Rails 5 model:
def self.payable
open.where.not(:delivery_status => "draft")
end
def self.draft
where(:delivery_status => "draft")
end
def self.open
where(:payment_status => "open")
end
Is there a more elegant way to write the first method?
It would be great to chain the open and draft methods together like this:
def self.payable
open.not(:draft)
end
Unfortunately, this doesn't work.
To chain negated queries you can use this trick:
def self.payable
open.where.not(id: draft)
end
Another alternative if you don't care if an ActiveRecord::Relation object is returned is using -, which returns an Array:
def self.payable
open - draft
end
I would personally use scopes instead of class methods for queries: https://guides.rubyonrails.org/active_record_querying.html#scopes. So:
scope :draft, -> { where(:delivery_status => "draft") }
scope :open, -> { where(:payment_status => "open") }
scope :payable, -> { open.where.not(id: draft) }
Maybe you can use scopes?
scope :payable, -> { open.where.not(:delivery_status => "draft") }
You can use this like that
YouModel.payable
I'd like to combine two scope condition with an inclusive or.
I tried with: scope :myscope, lambda { |u| where(cond1: u.id) or where(cond2: u.id)}, but it doesn't work. What can I do?
ActiveRecord provides some methods that bypasses the need to write SQL, but in this case you'll need to go put a very little effort and write a small piece of SQL.
scope :myscope, -> { |u| where("cond1 = ? OR cond2 = ?", u.id, u.id) }
You can also be more concise and use
scope :myscope, -> { |u| where("cond1 = :id OR cond2 = :id", id: u.id) }
There is nothing wrong in writing some SQL. Don't fall into the trap "if I can't write it in Ruby, it's ugly or not the Rails way".
There is a query method #or which you can use like this:
where(cond1: u.id).or(cond2: u.id)
Rails 5 will implement "OR" as #jphager2's answer stated, but meanwhile you have to get your hands a little dirty :
scope :myscope, lambda {where ["cond1 = ? OR cond2 = ?", u.id, u.id]}
You can do by this:-
scope :myscope, lambda { |u| where('cond1 = ? OR cond2 = ?', u.id, u.id)}
I have a two scopes in my user model:
scope :hard_deactivated, where(:hard_deactivated => true)
scope :soft_deactivated, where(:soft_deactivated => true)
So far so good
OR
I want to create a scope :deactivated, which will include all users where hard_deactivated is true OR soft deactivated is true. Obviously I could just do this:
scope :deactivated, where("hard_deactivated = ? or soft_deactivated = ?", true, true)
but this does not feel very dry.
NOT
Also I would like to create an inverse scope :not_hard_deactivated. I could do this:
scope :not_hard_deactivated, where(:hard_deactivated => false)
but again, this feels bad, especially if my scope becomes more complex. There should be some way or warpping the SQL generated by the previous scope in a not clause.
Use an arel table:
hard_deactivated_true = arel_table[:hard_deactivated].eq(true)
soft_deactivated_true = arel_table[:soft_deactivated].eq(true)
scope :deactivated, where(hard_deactivated_true.and(soft_deactivated_true))
scope :not_hard_deactivated, where(hard_deactivated_true.not)
See: Is it possible to invert a named scope in Rails3?
For the "NOT" part, you can do something like this:
extend ScopeUtils
positive_and_negative_scopes :deactivated do |value|
where(:hard_deactivated => value)
end
And implement this method in a separate module:
module ScopeUtils
def positive_and_negative_scopes(name)
[true, false].each do |filter_value|
prefix = ("not_" if filter_value == false)
scope :"#{prefix}#{name}", yield(filter_value)
end
end
end
Regarding the "OR" case, you might be something similar, depending on what your recurring pattern is. In the simple example above it's not worth it, as doesn't help readability.
scopes_with_adjectives_and_negatives :deactivated, [:soft, :hard]
module ScopeUtils
def scopes_with_adjectives_and_negatives(name, kinds)
kinds.each do |kind|
positive_and_negative_scopes name do |filter_value|
where("#{kind}_#{name}" => filter_value)
end
end
scope :"#{name}", where(kinds.map{|kind| "#{kind}_#{name} = ?"}.join(" OR "), true, true)
scope :"not_#{name}", where(kinds.map{|kind| "#{kind}_#{name} = ?"}.join(" AND "), false, false)
end
end
You should use sql snippet in where method (like in your second example), or more 'sugar' gems like squeel
I am showing a list of questions when the user use the index action. I want to filter this list, showing only rejected questions, questions that only have images attached to them etc.
How do you do that? Do you just add code in the index action that checks if the different named parameters is in the request parameter hash and use them build a query.
myurl.com/questions?status=approved&only_images=yes
Or are there better ways?
You can use has_scope to do this elegantly:
# Model
scope :status, proc {|status| where :status => status}
scope :only_images, ... # query to only include questions with images
# Controller
has_scope :status
has_scope :only_images, :type => boolean
def index
#questions = apply_scopes(Question).all
end
To keep your controller thin and avoid spaghetti code you can try to use following way:
Controller:
def index
#questions = Question.filter(params.slice(:status, :only_images, ...) # you still can chain with .order, .paginate, etc
end
Model:
def self.filter(options)
options.delete_if { |k, v| v.blank? }
return self.scoped if options.nil?
options.inject(self) do |scope, (key, value)|
return scope if value.blank?
case key
when "status" # direct map
scope.scoped(:conditions => {key => value})
when "only_images"
scope.scoped(:conditions => {key => value=="yes" ? true : false})
#just some examples
when "some_field_max"
scope.scoped(:conditions => ["some_field <= ?", value])
when "some_field_min"
scope.scoped(:conditions => ["some_field >= ?", value])
else # unknown key (do nothing. You might also raise an error)
scope
end
end
end
So, I think there are places where you need to code to be good in such a scenario; the model and the controller.
For the model you should use scopes.
#Model
scope :rejected, lambda { where("accepted = false") }
scope :accepted lambda { where("accepted = true") }
scope :with_image # your query here
In the controller,
def index
#questions = #questions.send(params[:search])
end
You can send the method name from the UI and directly pass that to the scope in the model. Also, you can avoid an "if" condition for the .all case by passing it from the UI again.
But as this directly exposes Model code to view, you should filter any unwanted filter params that come from the view in a private method in the controller using a before_filter.
I have to parameters: name and age
def self.search(name, age)
end
If name is not empty/nil, then add it to the search expression.
If age is not empty/nil, then add it to the search expression.
if both are nil, return all.
So far I have:
def self.search(name, age)
if name.nil? && age.nil?
return User.all
end
end
Finding it hard to write this in a elegant way.
def self.search(name, age)
conditions = {}
conditions[:name] = name if name
conditions[:age] = age if age
User.find(:all, :conditions => conditions)
end
I'm not sure what sort of search you're doing but I prefer to handle these sort of things in a scope.
class User < ActiveRecord::Base
scope :by_name, lamba{|name| name.nil?? scoped : where(:name=>name) }
scope :by_age, lamba{|age| age.nil?? scoped : where(:age=>age) }
def self.search(name, age)
User.by_name(name).by_age(age)
end
end
It's a bit more code overall I suppose but it's more reusable and everything is in it's place.
In Rails 3 you can do it like this:
def self.search(name, age)
scope = User
scope.where(:name => name) if name.present?
scope.where(:age => age) if age.present?
scope
end
Note the use of present? rather than nil? to skip empty strings as well as nil.
In the other comment you mentioned wanting to OR these conditions. ActiveRecord does not provide convenient facilities for that; all conditions are AND by default. You will need to construct your own conditions like so:
def self.search(name, age)
scope = User
if name.present? && age.present?
scope.where('name = ? OR age = ?', name, age)
else
scope.where(:name => name) if name.present?
scope.where(:age => age) if age.present?
end
scope
end