Date overlap unique per user rails - ruby-on-rails

I read this question Date range overlap per user rails
How can you validate this per user using this code below:
def no_reservation_overlap
if (Reservation.overlapping(date_start, date_end).any?)
errors.add(:date_end, 'it overlaps another reservation')
end
end
I used this code below which is the answer but it doesn't validate the conflict inbetween dates
booked june 2 to 3 created another booking 1 to 5 and it created how can i prevent that from happening?
validate :no_reservation_overlap
def no_reservation_overlap
if (Reservation.where("(? BETWEEN date_start AND date_end OR ? BETWEEN date_start AND date_end) AND user_id = ?", self.date_start, self.date_end, self.user_id).any?)
errors.add(:date_end, 'it overlaps another reservation')
end
end

Related

How to validate range dates with rails 6

hello i have a post module where the user can select the dates for publishing his posts, i have 2 fields
start_date:date and expiration_date:date
i want to make a validation like this
if user selects start_date = 2022-10-14 and expiration_date = 2022-10-22, he can't create another post that is between that range.
because i can have only 1 post published at a time, so with this validation i will force the user to select any other date that is not in between this selected range dates.
Just check that there is no other post that starts before the expiration date and ends after the start date. Also exclude this post's id in your check in case you're updating an existing post. (The post shouldn't prevent itself from changing).
This will catch posts that overlap the current post completely or partially, or that start and end within the current post.
validates :date_range
def date_range
if user.posts.where.not(id: id).where(start_date: ..expiration_date, expiration_date: start_date..).any?
errors.add(:start_date, 'there is already a post that overlaps these dates')
end
end
Inside you Post model you'll need a some validation.
You can create a custom method that will check whether the post you're trying to create has a start date between any current post.
class Post < ApplicationRecord
validate :post_exists? on: :create
private
def post_exists?
return unless Post.where("? BETWEEN start_date AND expiration_date", self.start_date).present?
errors.add(:start_date, "Post already exists")
end
end
I'm unable to test this at the moment, but its roughly what you'll need.
Some light reading on the on the subject ~
Custom validate methods: https://guides.rubyonrails.org/active_record_validations.html#custom-methods
Validating on certain actions, i.e. on: :create: in this case.
https://guides.rubyonrails.org/active_record_validations.html#on
It's worth mentioning there are some very similar other questions on stack, worth a google.
Rails ActiveRecord date between
So if I understand correctly you are looking to ensure there are no "overlaps". This entails ensuring all of the following are true
New start_date is not BETWEEN (inclusive) an existing start_date and expiration_date
New expiration_date is not BETWEEN (inclusive) an existing start_date and expiration_date
New start_date is not prior to an existing start_date and New expiration_date is not after the corresponding existing expiration_date
To satisfy these rules I would implement as follows:
class Post < ApplicationRecord
validates :start_date, presence: true,comparison: { less_than: :expiration_date}
validates :expiration_date, presence: true, comparison: { greater_than: :start_date }
validate :non_overlapping_date_range
def time_frame
start_date..expiration_date
end
private
def non_overlapping_date_range
overlaps_post = Period
.where(start_date: time_frame )
.or(Post.where(expiration_date: time_frame ))
.or(Post.where(start_date: start_date..., expiration_date: ...end_date))
.where.not(id: id)
.exists?
errors.add(:base, "overlaps another Post") if overlaps_post
end
end
This will result in the following query
SELECT 1 AS one
FROM
posts
WHERE
((posts.start_date BETWEEN '####-##-##' AND '####-##-##'
OR posts.expiration_date BETWEEN '####-##-##' AND '####-##-##')
OR (
posts.start_date > '####-##-##'
AND posts.expiration_date < '####-##-##'
))
AND posts.id != # -- IS NOT NULL on create
LIMIT 1
Using OVERLAPS (Postgres)
Postgres offers a function called OVERLAPS however the this does not fully fit the desired situation because this function treats end_date == new_start_date as continuous rather than overlapping. To counteract this we need to adjust the start_date and end_date for the purposes of the query
This can be achieved as follows:
def non_overlapping_date_range
overlaps_post = Post.where.not(id: id)
.where('(?,?) OVERLAPS (posts.start_date, posts.expiration_date)',
start_date - 1, end_date + 1))
.exists?
errors.add(:base, "overlaps another Post") if overlaps_post
end
SQL:
SELECT 1 AS one
FROM
posts
WHERE
('####-##-##','####-##-##') OVERLAPS (posts.start_date,posts.expiration_date)
AND posts.id != # -- IS NOT NULL on create
LIMIT 1
Arel port of the same:
def non_overlapping_date_range
left = Arel::Nodes::Grouping.new(
[Arel::Nodes::UnaryOperation.new(
'DATE',
[Arel::Nodes.build_quoted(start_date - 1)]),
Arel::Nodes::UnaryOperation.new(
'DATE',
[Arel::Nodes.build_quoted(expiration_date + 1)])])
right = Arel::Nodes::Grouping.new(
[Post.arel_table[:start_date],
Post.arel_table[:expiration_date]])
condition = Arel::Nodes::InfixOperation.new('OVERLAPS', left, right)
errors.add(:base, "overlaps another Post") if Post.where.not(id: id).where(condition).exists?
end

How to check if a booking between two dates is possible

An Offer has a number of places. It means that each day places bookings can be made for this offer. An Offer has_many bookings.
A Booking has a date_begin, a date_end and belongs_to an Offer. It means that each day between date_begin and date_end one place is used regarding the associated Offer.
Given a requested booking for an offer, how should I check if it can be validated?
Example :
- offer has 2 places (offer.places == 2).
- offer has currently 3 bookings (offer.bookings.count == 3)
- First booking B1 is between 01-04-2016 and 10-04-2016
- Second booking B2 is between 01-04-2016 and 05-04-2016
- Third booking B3 is between 08-04-2016 and 10-04-2016.
offer.available_between?("01-04-2016", "10-04-2016")
=> false (because of B1 and B2, and because of B1 and B3)
offer.available_between?("01-04-2016", "05-04-2016")
=> false (because of B1 and B2)
offer.available_between?("08-04-2016", "10-04-2016")
=> false (because of B1 and B3)
offer.available_between?("06-04-2016", "07-04-2016")
=> true (because there is only B1 during this period)
Here is a try:
class Offer < ActiveRecord::Base
# True if and only if each day between date_begin and date_end has at least one place left.
def available_between?(date_begin, date_end)
(date_begin.to_datetime.to_i .. date_end.to_datetime.to_i).step(1.day) do |date|
day = Time.at(date)
nb_places_taken_this_day = self.bookings.where("date_begin <= :date AND date_end >= :date", date: day).count
return false if nb_places_taken_this_day >= self.places
end
true
end
end
offer.available_between?(booking.date_begin, booking.date_end)
I don't feel confortable with this at all especially because there are multiple separated SQL queries.
Can you see a better way to achieve this using ActiveRecord more efficiently?
I don't have easy enough data to test this throughly, but I'm thinking something like this can work. Here I'm building into your query the condition that no booking exists where the start and end dates are not either both before or both after the requested dates. That is, every booking that does exist had better have either start and end before the requested start, or start and end after the requested end. If there exists a booking that does not meet this condition, then the requested dates are not available as a single booking. Does this work?
[EDIT - modified from original broken answer to reflect the actual problem]
This new answer still loops over days, but does so after grabbing all the conflicting bookings in one query. The ruby loop itself will be pretty quick.
(Note that the :all? method will immediately return false on the first false value and will return true if all elements evaluate to true, so adding the return false unless as suggested by the edit I believe is superfluous. In any case I've added a return to the last line to hopefully clarify the intent.)
def available_between?(date_begin, date_end)
conflicting_bookings = self.bookings.where.not("(date_begin < :requested_start_date AND date_end < :requested_start_date) OR (date_end > :requested_end_date AND date_end > :requested_end_date)", requested_start_date: date_begin, requested_end_date: date_end)
return (date_begin..date_end).all? do |day|
num_bookings_for_day = conflicting_bookings.select{|booking| booking.date_begin <= day && booking.date_end >= day}.count
num_bookings_for_day < self.places
end
end

How can I check if a new event (start time - endtime) doesn't overlap with previous startime - endtimes (Ruby)

I am making an appointment, scheduling API. I have many starttime and endtime pairings in DateTime format. I need to be sure that when I create a new appointment that times do not overlap with previous ones. What I mean is that if I have an appointment starting at 7/4/15 9:00 and ending at 7/4/15 13:00 I want to make a validation so that I can't make a new appintment starting at 7/4/15 10:00 ending at 7/4/15 12:00. I want to compare all the key value pairs to make sure the new one doesn't fall inside that range. Any ideas how I can do this?
An overlap happens when you have an a appointment that starts before this appointment ends, and ends after this one starts. If there are no appointments that fit that criteria, there are no overlaps.
Note that you need to also consider the special case that searching for appointments will find one overlap but it's the appointment you're currently editing, so you can ignore that one.
class Appointment < ActiveRecord::Base
validate :no_overlapping_appointments
def no_overlapping_appointments
overlaps = Appointment.where('start_time <= ? AND end_time >= ?', end_time, start_time)
return if overlaps.empty?
return if overlaps.count == 1 && overlaps.first.id == id
errors.add(:start_time, "This appointment overlaps others")
end
end
class Appointment < ActiveRecord::Base
validate :duration_not_overlap
private
def duration_not_overlap
verify_time(:starttime)
verify_time(:endtime)
end
# attr is :starttime | :endtime
def verify_time(attr)
errors[attr] << 'overlap' if Appointment.where(user_id: user_id, attr => (starttime..endtime)).exists?
end
end

Rails: How to use includes with conditions?

I have a ressource Room that has_many reservations. Likewise the ressource Reservation belongs_to a room.
My reservation model has a starts_at and an ends_at attribute.
In my index action for the Room controller I want to get all the Rooms and include all the reservations for a certain date(the date is given as a parameter).
I have tried this code, but it doesn't seem to work as intended.
#rooms = Room.includes(:reservations).where("reservations.starts_at" => params[:date])
I am aware that this code does not take into account that a room could be reserved for multiple days, but I had to start somewhere.
SUM UP
Based on a date the action should return all the rooms, but only include the reservations that is relevant for that date.
EDIT
This is what I ended up doing.
controllers/rooms_controller.rb
def index
#rooms = Room.includes(:reservations)
#date = params[:date] ||= Date.today
end
views/rooms/index.html.haml
- #rooms.each do |room|
- room.reservations.relevant(#date).each do |reservation|
= reservation.id
models/reservation.rb
def self.relevant(date = Date.today)
if date.blank?
date = Date.today
end
where(
'(starts_at BETWEEN ? AND ?) OR (ends_at BETWEEN ? AND ?)',
date.to_date.beginning_of_day, date.to_date.end_of_day,
date.to_date.beginning_of_day, date.to_date.end_of_day
)
end
It works alright, but the view is talking to the model I think?
If your where conditions refer to another table then you also need to need to specify references as well as includes. e.g.
#rooms = Room.includes(:reservations).
where("reservations.starts_at" => params[:date]).
references(:reservations)
See the API documentation here.

Validate Date in Rails before it hits the database

I need to validate two dates in date time format that come from create new record form. Right now the form has drop downs for year, date, month, hour, minute. In the controller, I need to validate that the start date is not greater than end date and it will not let me compare it using the params[:start_date] > params[:end_date].
How can I properly validate that the start date is not larger than the end date when adding a new record to the database, I should be doing this in the model but I cannot figure out how you do it. Does anyone here has any examples I can look from?
Add custom validation to your model to verify that the start date is less than the end date. Something like this would work:
# app/models/my_model.rb
validate :dates_in_order
def dates_in_order
errors.add(:start_date, "must be before end time") unless start_date < end_date
end
#some_model.rb
before_create -> {
errors.add(:base, "Start date cannot be later than end date.") if start_date > end_date
}
Not what you're asking for, but may also be a way to handle this. People sometimes don't read the labels so closely.
before_create :confirm_dates_in_order
def confirm_dates_in_order
start_date, end_date = end_date, start_date if start_date > end_date
end

Resources