RAILS - SCOPE FILTER THROUGH 2 TABLES
Hi, I'm new to Rails and I'm using Translate to post here.
I have the scenario below:
Ata_Public -> Ata_Object -> Products
How can I set up a filter to select all Ata_publics through the category_id inside Products?
scope :filter_category, -> (params) {
params[:category_id].present? ?
joins(:ata_objects)
.joins(:products)
.where("ata_objects.products.category_id = ?", params[:category_id])
:
all
}
I'm doing it this way, but I don't have the expected result.
What should my scope look like?
Relationships are right.
clarifying, inside the Ata_Public model, I'm creating this scope: filter_category, to pass the ID and list only the minutes that have the product with this category_id.
https://i.stack.imgur.com/V0I0s.png
This should work:
scope :filter_category, -> (params) {
joins(ata_objects: :products).where(
products: { category_id: params[:category_id] }
) if params[:category_id].present?
}
Related
I have a requirement where the user can type in a search box and the Rails api should search any of the customer fields for a possible match, so I started like this and realised this was not such a great solution and seemed quite repetitive for all 5 fields:
scope :filter, -> (term) { where(
"lower(customers.name) LIKE ? OR
lower(customers.email) LIKE ? OR
lower(customers.business_name) LIKE ? OR
lower(customers.phone) LIKE ? OR
lower(customers.doc_id) LIKE ? OR",
"%#{term.downcase}%", "%{term.downcase}%", "%#{term.downcase}%",
"%#{term.downcase}%", "%#{term.downcase}%"
) }
So I learned about Arel and tried this instead:
customers = Customer.arel_table
scope :filter, -> (term) { Customer.where(
customers[:name].matches("%#{term.downcase}%")).
or(customers[:email].matches("%#{term.downcase}%")).
or(customers[:phone].matches("%#{term.downcase}%")).
or(customers[:business_name].matches("%#{term.downcase}%").
or(customers[:doc_id].matches("%#{term.downcase}%"))
) }
but that is just as repetitive.
Is there a way to simply either version? I was thinking maybe for Arel I could do this:
scope :filter, -> (term) { Customer.where(
customers[:name, :email, :phone, :business_name, :doc_id].matches("%#{term.downcase}%")
) }
UPDATE
Apologies but I forgot to mention - I was trying to keep this simple! - that if there is a simpler solution, it would still need to be a chainable scope, because I am using this filter in a chain of other scopes, like this in the controller:
if params[:filter].present?
#cards = current_api_user.account.cards.new_card(:false).search(params.slice(:filter))
else ...
where 'search' is a concern that simply sends the filter params key/value pair to scopes in the model. For example, here is the cards model scopes (you can see it's filter scope then calls the filter_customer scope, which then calls Customer.filter which is the one the question is about). This might seem complex but it means I have complete composability of all scopes for all these related models:
scope :new_card, -> value { where(is_new: value) }
scope :filter_template, -> (term) { Card.where(template_id: Template.filter(term)) }
scope :filter_customer, -> (term) { Card.where(customer_id: Customer.filter(term)) }
scope :filter, -> (term) { Card.filter_customer(term).or(Card.filter_template(term)) }
Option 1:
Build a condition string with many ORs
fields = ["name", "email", "phone", "business_name", "doc_id"]
filter = fields.map { |field| "lower(#{field}) LIKE '#{term.downcase}'" }.join(' OR ')
#customers = Customer.where(filter)
Option 2:
Concatenate searches using simple conditions
fields = ["name", "email", "phone", "business_name", "doc_id"]
#customers = []
fields.each do |field|
filter = "lower(#{field}) LIKE '#{term.downcase}'"
#customers.concat(Customer.where(filter))
end
Scope:
With a small change you can use the first method as a scope
Class Customer
scope :filter_customer, -> (term) { Customer.where(Customer.build_filter(term)) }
def self.build_filter term
fields = ["name", "email", "phone", "business_name", "doc_id"]
filter = fields.map { |field| "lower(#{field}) LIKE '#{term.downcase}'" }.join(' OR ')
end
Notes: Your first post was based on Customer and I made all code based on this model. After your update, the answer needs some changes to use in Cards, but it should be trivial.
I would like to combine two different scopes in my model. I have this:
Post_model
scope :with_tasks, -> { where(cat: 3).includes(:user).includes(task: :users) }
scope :with_events, -> { where(cat: 4).includes(:user).includes(event: :users) }
scope :with_comments, -> {where(comented: true).includes(comments: :user)}
Post_controller
def index
#posts = current_user.posts.with_tasks + current_user.posts.with_events
end
But I think it is not a really elegant way to achieve it, and I cannot include the comments scope.
Do you know a method to join this scopes into a new one (like the example below)?
scope :with_elements, -> { self.with_tasks.merge(self.with_events) }
What would allow me to call this method into my post#index:
#posts = current_user.posts.with_elements
TASKS = 3
EVENTS = 4
scope :with_tasks_and_or_events, ->(cat) {
cond = {}.tap do |c|
c.merge!(task: :users) if cat.include? TASKS
c.merge!(event: :users) if cat.include? EVENTS
end
where(cat: cat).includes(:user).includes(**cond)
}
And use it like:
with_tasks_and_or_events([TASKS])
with_tasks_and_or_events([TASKS, EVENTS])
Or, better, use Relational Algebra.
Or, even better, revise your database structure.
The following code gives an empty scope. Category_ids is an array of categories.
scope :art, ->{ where(:category_ids => '1') }
How do I check to see if one of the categories exist in the array?
If you use Postgres you can use this approach: https://www.viget.com/articles/searching-serialized-fields-in-rails-using-postgres-arrays
get the categories
correct your where query
Example:
has_many :categories
scope :art, -> { required = [Category.first]; where(categories: required) }
I assume that in your model, you have categories association. In this case, you can just use categories: required in your where query. required should be set to an array of categories which you wanted
You say that category_ids is an array of categories(I'm assuming category id's). Are you trying to return all records with a category ID that is in that array? If so you're looking for:
scope :art, -> { where (:category_id => category_ids) }
Or with the new ruby syntax:
scope :art, -> { where(category_id: category_ids) }
If I've misunderstood and you're looking for any record with a category ID of 1, then you're looking for:
scope :art, -> { where(category_id: '1') }
I have two tables that are something like:
users
id
name
active
items
id
user_id
color
Using Rails, I want to select the active users along with the number of items that are red or blue.
Something like:
User.where(active: true).joins(:items).where(items: {color: ['red', 'blue']}).count(:items)
I want the result to be an array of Users where the Users have an annotated number of items.
So it could end up like users = activerecord query, users.first.name == 'John', users.first.items_count == 3
What would you do?
Considering the color filter, I'd just do the count in ruby.
class User
scope :active, -> { where(active: true) }
def items_of_color(colors)
items.select{ |i| i.color.in?(colors) }
end
end
in the controller
#users = User.active.preload(items)
and then in the view, count the red and blue
user.items_of_color(['red', 'blue']).size
But, if RED and BLUE are special and commonly referenced, you can do this...
class User
...
has_many :red_and_blue_items, where -> ({color: ["red", "blue"]}), class_name: "Item"
end
And then, preload like so
#users = User.active.preload(:red_and_blue_items)
In the view
#users.first.red_and_blue_items.size
I don't say it is the solution, but this following statement
Item
.joins(:user)
.group(:user_id)
.where(users: { active: true }, color: ['red', 'blue'])
.count
return a list of user_id with its associated item count:
{
user_id_1 => count_1,
user_id_2 => count_2,
user_id_3 => count_3,
...
}
I am noticing a trend with my scopes and trying to figure out how to make it dry
scope :newest, -> { order('created_at DESC') }
scope :top_sold, -> { order('qty_sold DESC') }
scope :most_viewed, -> { order('qty_viewed DESC') }
scope :most_downloaded, -> { order('qty_download DESC') }
scope :most_favorited, -> { order('qty_favorited DESC') }
I would like to pass in the column I want sorted so that I can call it on Photo. I tried this, but running into problems
scope :sort_photos, -> type { order('type DESC') }
Photo.sort_photos('qty_download')
Am I on the right path or is there a smarter way to accomplish this?
Pass type as a scope parameter and use that in order clause with string interpolation:
scope :sort_photos,->(type) { order("#{type} DESC") }
Then do:
Photo.sort_photos('qty_download')
The order method takes a String or a Hash. So instead of order('created_at DESC') you can do order(created_at: :desc), for example. So, to accomplish what you want, it's as simple as changing the key to your type variable:
scope :sort_photos, -> type { order(type => :desc) }
I would also recommend using a sentinel for your order scopes such as by_. So that the scope by_sort_photos doesn't get overridden by definition of a sort_photos method or assoication later.
Finally, it's good to have a public interface full of methods, as opposed to requiring knowledge of the class attributes and passing those attribute names into a public interface method. So I'd keep the many different scopes that you have, but perhaps have them all refer to the one, general scope as we've defined here. So:
scope :newest, -> { by_most_recent_type('created_at') }
scope :top_sold, -> { by_most_recent_type('qty_sold') }
scope :by_most_recent_type, -> type { order(type => :desc) }