Rails finding all items in a joins that comply with multiple conditions - ruby-on-rails

I have Venue, Review and Voucher models. A Venue has_many Reviews, and a Review has_one Voucher.
A voucher has a boolean claimed field.
I'm trying to build a query to select all claimed: true Vouchers that are before a certain date, and to return the length of that query.
So far I've tried a few varieties of the following query:
# #venue is an instance of a Venue
#venue.reviews.joins(:voucher)
.where(vouchers: { claimed: true })
.where("created_at < ?", Date.today.beginning_of_week - 7)
.length
However I just get a vague ActiveRecord::StatementInvalid error, with my .length being highlighted. When I try the above query without the .length in the console, I get a <Review::ActiveRecord_AssociationRelation>

If you want the count of vouchers with claimed true and the right timestamp you can just do:
Voucher.where(vouchers: { claimed: true })
.where("created_at < ?", Date.today.beginning_of_week - 7)
.length
Now if you want that for a specific venue, you still want to start the query with voucher and then join the venue table onto it:
Voucher.joins(review: :venue)
.where(venue_id: #venue.id)
.where(vouchers: { claimed: true })
.where("created_at < ?", Date.today.beginning_of_week - 7)
.length
You might need to play around a bit with .joins(review: :venue) you can find more information here: https://guides.rubyonrails.org/active_record_querying.html#joining-tables

Related

Ruby on Rails Active Record query joining two tables and query based on condition

I have two tables: Transactions and Properties.
I have one condition to satisfy that doesn't require joining tables.
On my Transactions query:
rows where sales_date is in a certain month
rows where sold_or_leased is "leased"
My next condition requires joining properties to transactions so that I can:
rows where transactions.sales_date is in a certain month
rows where transactions.sold_or_leased is null AND
rows where properties.for_sale is false AND properties.for_lease is true
Basically, a new column was added to transactions called sold_or_leased and a lot of them are null. I need to an extra query to cover the null columns.
#test variables for month
date = "2019-11-01"
month = Date.parse date
# below satisfies my first part
#testobj = Transaction.where(sold_or_leased: "leased")
.where("sales_date >= ? AND sales_date < ?", month.beginning_of_month, month.end_of_month).count
But now I need to extend this query to include properties and test a property column
I'm not sure where to go from here:
#testobj = Transaction.joins(:property)
.where(sold_or_leased: "leased")
.where("sales_date >= ? AND sales_date < ?", month.beginning_of_month, month.end_of_month)
.or(
Transaction.where(sold_or_lease: nil)
).count
Also, when I add a join and then an or clause, i get an error Relation passed to #or must be structurally compatible. Incompatible values: [:joins]
I will share relevant model info:
Transaction Model:
class Transaction < ApplicationRecord
belongs_to :user
belongs_to :property
end
Property Model:
class Property < ApplicationRecord
has_one :property_transaction, class_name: 'Transaction', dependent: :destroy
end
With the help of Sebastian, I have the following (which still produces the structural error message):
Transaction.joins(:property)
.where(sales_date: month.all_month,
sold_or_leased: nil,
properties: { for_sale: false, for_lease: true })
.or(
Transaction.joins(:property)
.where(sold_or_leased: "leased")
.where("sales_date >= ? AND sales_date < ?", month.beginning_of_month, month.end_of_month)
)
In theory, you should be able to access the properties table columns after a join.
Looking to your current code and what you need to get you could try with:
Transaction
.joins(:property)
.where(sales_date: month.all_month)
.where(
"(sold_or_leased IS NULL AND properties.for_sale = false AND properties.for_lease = true) OR
(sold_or_leased = 'leased')"
)
If you're unable to use the ActiveRecord::QueryMethods#or, you can always use the SQL OR operator within a string argument to where.
Notice month.all_month produces the whole range of dates for a corresponding month, which when used with where is converted to the first and last day of the month:
SELECT ... WHERE "transactions"."sales_date" BETWEEN $1 AND $2 AND ... [["sales_date", "2019-11-01"], ["sales_date", "2019-11-30"]]
Shorter than the month.beginning_of_month and month.end_of_month variation.

Constructing Query with Rails - GROUP, MAXIMUM, UNION

I have two models:
Student
-> has_many :enrollments
Enrollment
-> belongs_to :student
The tables goes something like this:
https://i.stack.imgur.com/ilIxL.png
Now my goal is to retrieve those students which latest enrollment end date is lesser than the current date, or if the student has no enrollment yet. So given the sample table on the link above, and current date is 11/20/2019, the query should return:
John Doe (because latest end date is 11/01/2019 which is lesser than 11/20/2019)
Casper Gost (because this student has no enrollment record yet)
How do I create that query in Ruby on Rails? (Sorry, I'm still noob in rails).
I visualize the sql query is this:
select students.*
from students s
left join
( select student_id, end_date
from enrollments e1
where end_date = (select max(e2.end_date)
from enrollments e2
group by e2.student_id
having e2.student_id = e1.student_id
)
) e on e.student_id = s.id
where e.end_date < Date.today OR e.student_id IS NULL
but I can't seem to build it using Rails ActiveRecord's methods.
EDIT
I tried Roc khalil's/Catmal's solution, but the "or" method is giving an incompatible error:
(Relation passed to #or must be structurally compatible. Incompatible
values: [:includes])
Also, I revised the code a little bit to fit my needs:
This will retrieve all students that have no records in enrollments table
Student.where.not(id: Enrollment.pluck(:student_id))
And this code will retrieve all students whose last enrollment's end date is lesser than today
Student.joins(:enrollments).where('end_date IN (?)',Enrollment.all.group(:student_id).maximum(:end_date).values).where('end_date < ?', Date.today) When I executed them separately, I get no error. However if I combine them with the "or" method I get the same incompatible error:
ArgumentError (Relation passed to #or must be structurally compatible.
Incompatible values: [:joins])
I've searched it and it looks like it's a bug. :(
So close though :(
UPDATE
I have implemented both #nathanvda and #Catmal suggestion, and came up with this:
Student model:
scope :no_enrollments, -> { where.not(id: Enrollment.pluck(:student_id)) }
scope :with_expired_enrollments, -> { joins(:enrollments).merge(Enrollment.expired_enrollments) }
scope :unenrolled, -> { find_by_sql("#{no_enrollments.to_sql} UNION #{with_expired_enrollments.to_sql}") }
Enrollment model:
scope :expired_enrollments, -> { where('end_date IN (?)',Enrollment.all.group(:student_id).maximum(:end_date).values).where('end_date < ?', Date.today) }
So in my controller, to get the unenrolled students, I just have to call:
Student.unenrolled
Maybe not the optimum solution, but it works on my app. Thanks #nathanvda and #Catmal.
Try this:
#selected_students = Student.where.not(
id: Enrollment.pluck(:student_id)
).or(
Student.includes(:enrollments).where(
enrollments: {
date_end < Date.today
}
)
)

ActiveRecord: Get all users who should be banned today

I have two models:
class User < ActiveRecord::Base
has_many :ban_messages, dependent: :destroy
end
class BanMessage < ActiveRecord::Base
belongs_to :user
end
Table BanMessage contains a field t.datetime :ban_date that stores the date, when user should be banned. User table contains a field status, that contains one of the status values (active, suspended, banned). When I send BanMessage to User, his status field changes from 'active' to 'suspended'. Also I can activate user back, so he would not be banned at ':ban_date', but his BanMessage wouldn't be destroyed.
So I need to create query for selecting all Users with status 'suspended', who have in their newest BanMessages records 'ban_date' field, with DateTime value, which is between 'DateTime.now.beginning_of_day' and 'DateTime.now.end_of_day'.
You can filter your users with where query followed by join method for associated ban_messages with where method to further filter with the date range.
User.where(status: 'suspended').joins(:ban_messages).where("ban_date >= ? AND ban_date <= ?", DateTime.now.beginning_of_day, DateTime.now.end_of_day )
Try something like this.
User.joins(:ban_messages).where(status: :suspended).where('ban_messages.ban_date BETWEEN :begin_time AND :end_date', begin_time: DateTime.now.beginning_of_day, end_time: DateTime.now.end_of_day)
Here's what I needed:
User.joins(:ban_messages)
.where('users.status = ?', :suspended)
.where('ban_messages.created_at = (SELECT MAX(ban_messages.created_at) FROM ban_messages WHERE ban_messages.user_id = users.id)')
.where('ban_messages.ban_date BETWEEN ? and ?', DateTime.now.beginning_of_day, DateTime.now.end_of_day)
.group('users.id')
UPDATE
MrYoshiji: The caveat in your code is that if you miss one day, then
the next day you won't see the people that should have been banned the
day before. You might want to get all User where the ban_date is
lesser than DateTime.now.end_of_day, either in the same view or
another one.
So final query might be something like this
User.joins(:ban_messages)
.where('users.status = ?', :suspended)
.where('ban_messages.created_at = (SELECT MAX(ban_messages.created_at) FROM ban_messages WHERE ban_messages.user_id = users.id)')
.where('ban_messages.ban_date < ?', DateTime.now.end_of_day)
.group('users.id')

has_many and No method error issue

I have two tables: Stores and products. The store model has a has_many :products and the products has a belongs_to :store
I'm trying to do this though in the rails console:
Store.where(open: true).products.where("created_at <= ?", 1.month.ago)
and get the error (paraphrased a bit): NoMethodError: undefined method products for #<Store
Not a very easy thing to do - products is a method defined on an instance of Store and you are calling it on the relation. I would probably go with:
Product.where(store_id: Store.where(open:true).pluck(:id)).where("created_at <= ?", 1.month.ago)
which would generate two db calls, but also returns a clean and easy to query scope. Another approach would be to use join:
Product.joins(:store).where(store: { open: true }).where("created_at <= ?", 1.month.ago)
This will do the work with one query, but due to the join it won't be that easy to manipulate the resulting scope that easily.
You have it backwards. Since you can have many stores, Rails will not return all the products where open: true.
You need to join and lookup the products where the store is open.
Product.joins(:store).where(store: {open: true}).where("created_at <= ?", 1.month.ago)

How to return the next record?

In my Rails app I have a function next which I am using in my show templates to link to the next product (ordered by price):
class Product < ActiveRecord::Base
belongs_to :user
def next
Product.where("user_id = ? AND price > ?", user_id, price).order("price ASC").first
end
end
The problem is that this function works only when all products have different prices. The moment there are multiple products with the same price, the next function picks one product randomly (?) thereby skipping all the others.
Is there a way to make this function return the next product, even though it has the same price? (if the price is the same, the products could be ordered simply by ID or date)
I tried replacing > with >= but that didn't work.
Thanks for any help.
You have to use id in where clause and in order as well
def next
Product.where("user_id = ? AND price >= ? AND id > ?", user_id, price, id).order("total, id").first
end

Resources