Avoid double bookings for a minibus reservation system - ruby-on-rails

I'm trying to build a reservation system where a customer can reserve a minibus. I've been able to get the all the data so a booking can be made.
I'm trying to avoid another user to reserve the minibus for the same day. I'm not to sure how to go about it as in new to ruby on rails.
In my reservation.rb I've got
belongs_to :vehicle
belongs_to :user
In my user.rb and vehicle.rb I've got
has_many :reservation
In my reservation controller I've got
def new
#vehicle = Vehicle.find(params[:vehicle_id])
#reservation = Reservation.new(user_id: User.find(session[:user_id]).id)
#reservation.vehicle_id = #vehicle.id
end
would I use validation to stop double reservations?
would it be something like in my reservation.rb
validates :vehicle_id, :startDate, :uniqueness => { :message => " minibus already reserved"}
Although the above will only allow the vehicle to be reserved.
Any help will be much appreciated!

As you already figured out you cannot use Rails' built-in uniqueness validator to validate that two ranges do not overlap.
You will have to build a custom validation to check this. A condition that checks if two time or date ranges A and B overlap is quite simple. Have a look at this image.
A: |-----|
B1: |-----|
B2: |---------|
B3: |-----|
C1: |-|
C2: |-|
A and B overlap if B.start < A.end && B.end > A.start
Add the following to your model:
# app/models/reservation.rb
validate :reservations_must_not_overlap
private
def reservations_must_not_overlap
return if self
.class
.where.not(id: id)
.where(vehicle_id: vehicle_id)
.where('start_date < ? AND end_date > ?', end_date, start_date)
.none?
errors.add(:base, 'Overlapping reservation exists')
end
Some notes:
You might need to adjust the naming of the database columns and the attributes names because I wasn't sure if it was just a typo or if you use names not following Ruby conventions.
Furthermore, you might need <= and >= (instead of < and >), depending on your definition of start and end.
Moving the condition into a named scope is a good idea and will improve readability

You're gonna want to use the uniqueness validator lie you're already doing but use the scope option.
The example they give on that page is pretty similar to your use case:
class Holiday < ApplicationRecord
validates :name, uniqueness: { scope: :year,
message: "should happen once per year" }
end
As to which column you should validate, it doesn't really matter. Since the uniqueness scope is going to be all three columns, it can be any of them:
validates :vehicle_id, uniqueness, { scope: [:startDate, user_id], message: "your message" }
You should also add indexes to the database as described here (this question is very similar to yours by the way).

Related

Custom validator to prevent overlapping appointments in Rails 4 app?

I need help writing a custom validation to prevent overlapping appointments in a Rails 4 app. I'm coding this app to teach myself Ruby & Rails. While researching the issue, I discovered a gem called ValidatesOverlap, but I want to write my own validator for learning purposes.
My Appointment model has an "appointment_at" column of the datetime datatype and a "duration" column of the time datatype. The Appointment model has a "has_many :through" association with Member and Trainer models. Appointments:
belongs_to :member
belongs_to :trainer
Existing validations in the Appointment model include:
validates :member, uniqueness: {scope: :appointment_at, message: "is booked already"}
validates :trainer, uniqueness: {scope: :appointment_at, message: "is booked already"}
The custom validator needs to prevent members or trainers from scheduling overlapping appointments. Right now, I can prevent "duplicate appointments" from being saved to the database, but can't stop "overlapping" ones. For example, if trainer_1 is booked for a 1 hour appointment with member_1 which starts at 7:00 am, my model validations prevent member_2 from booking an appointment with trainer_1 for 7:00 am. However, I have no current means of preventing member_2 from scheduling a session with trainer_1 for 7:01 am! I'm working with two attributes: "appointment_at," which is the start time and "duration" which is the total time of an appointment. I'd prefer to keep those attributes/columns if I can easily calculate "end time" from the "appointment_at" and "duration" values. I haven't figured out how to do that yet :)
I'd appreciate any thoughts or suggestions about how I can approach solving the problem of overlapping appointments (without using a gem). Many thanks in advance!
I had the same problem a while ago. You need a scope :overlapping, that reads overlapping appointments for an appointment and a validator to check. This example is for a PostgreSQL DB. You have to adjust it for your DB, if you're using another DB.
class Appointment < ActiveRecord::Base
belongs_to :member
belongs_to :trainer
validate :overlapping_appointments
scope :overlapping, ->(a) {
where(%q{ (appointment_at, (appointment_at + duration)) OVERLAPS (?, ?) }, a.appointment_at, a.appointment_to)
.where(%q{ id != ? }, a.id)
.where(trainer_id: a.trainer.id)
}
def find_overlapping
self.class.overlapping(self)
end
def overlapping?
self.class.overlapping(self).count > 0
end
def appointment_to
(appointment_at + duration.hour.hours + duration.min.minutes + duration.sec.seconds).to_datetime
end
protected
def overlapping_appointments
if overlapping?
errors[:base] << "This appointment overlaps with another one."
end
end
end
Give it a try and let me know, if it helped you.

Rails 4: Alternative to strings when setting "type" / "status" attribute?

I've run into this situation many times, where I need to store something like a status persay, so it would be something like this:
class Order < ActiveRecord::Base
INCOMPLETE = 'Incomplete'
IN_PROGRESS = 'In progress'
SHIPPED = 'Shipped'
CANCELLED = 'Cancelled'
...
end
Order would have a status attribute, and when creating an order I would just use collection_select with [INCOMPLETE, IN_PROGRESS, SHIPPED, CANCELLED] as the options.
Is there a cleaner way of doing this without using hardcoded strings, like using a Status association or some sort of PORO? I feel like this would be a bit brittle, like if someone changed the INCOMPLETE = 'Incomplete' to INCOMPLETE = 'Incompletezzzzz' then all the statuses of my existing records would not match.
I would advise you to use another model in this situation, something like OrderStatus:
class OrderStatus < ActiveRecord::Base
validates :internal_reference, presence: true, uniqueness: true
has_many :orders
def translated(locale = :en)
I18n.t("activerecord.attributes.order_status.#{self.internal_reference}", locale: locale, default: self.internal_reference)
end
class Order < ActiveRecord::Base
belongs_to :order_status
validates :order_status_id, presence: true
And the records will look like this:
OrderStatus.first
# => OrderStatus id: 1, internal_reference: canceled
Order.first
# => Order id: 3, order_status_id: 1 # etc.
In your views, it could be:
order.order_status.translated(I18n.locale)
# looks for activerecord.attributes.order_status.canceled
# if nothing found, returns the internal_reference, here it would return `'canceled'`
This configuration is better than just constants :
You can create as many statuses as you want,
You can translate them directly (using internal_reference as key for I18n),
The statuses can be tested either if they are in english, french or whatever (thanks to internal_reference,
You can create statuses directly in your app, without rebooting it,
You can set an attribute, like status_code and makes ranges (kind of like HTTP requests statuses) and group them (ex: if status_code > 100)
You can also add an boolean attribute cant_be_deleted to prevent from deleting Statuses used in the code.
You might think it is overkill to do so, but I guarantee that the day you will want to translate / add / remove / change your Statuses, it will be much easier with Models rather than Constants. Trust me, I worked for an Online shop, handling Carts, Orders and Products, I know how painful it is to change from constants to models, everywhere in your already existing code ;-)
I think enumerize gem can help
You can write your code like this
class Order < ActiveRecord:Base
extend Enumerize
enumerize :status, in: [:incomplete, :in_progress, :shipped, :cancelled]
end
It also works perfectly with I18n.
https://github.com/brainspec/enumerize

One validation for many model or how to right to do validation

I have 7 model Attach that set period of attache date for other model. I need to validate that date of attache do not have intersection periud for example:
class AttachNetworkToUser < ActiveRecord::Base
attr_accessible :dt_begin, :dt_end, :network_id, :user_id
validates :dt_begin, :dt_end, :network_id, :user_id, :presence => true
belongs_to :network
belongs_to :user
validate :period_attach?
def period_attach?
user = AttachNetworkToUser.select("id, dt_begin, dt_end").where("network_id = :network_id " , { :network_id => self.network_id} )
user.each do |t|
if ( (self.dt_begin >= t.dt_begin and self.dt_begin <= t.dt_end) or (self.dt_end >= t.dt_begin and self.dt_end <= t.dt_end) ) or (self.dt_begin < t.dt_begin and self.dt_end > t.dt_end )
self.errors[:dt_begin] << " intersection periud! You can consolidate with " + (t.dt_end+1).to_s
end
if self.dt_begin > self.dt_end
self.errors[:dt_begin] << " can not be more dt_end"
end
end
end
end
It looks like code smell. And in other Attache model i have repeated code of this validation but with other parameters something like this:{:Attach=>self,:user_id=>self.user_id,:network_id=>self.network_id,:dt_begin=>self.dt_begin,:user_id=>self.user_id}
How correctly resolve this aim.
Maybe put validation code in other class and send parameter like this:AttachModelValidator.new({:Attach=>self,:user_id=>self.user_id,:network_id=>self.network_id,:dt_begin=>self.dt_begin,:user_id=>self.user_id}).valid
Normally I would say keep all validations in each of the models, in order for the code to be well understood.
Technically it is possible to do that:
If your fields had the same names in the various models, you could write all the validations once in a module, and put your models under that same module.
In case your fields names are different, you can define a method in a shared module, to receive the fields names and perform the validation.
Again, I would not recommend doing this. Unless you have a common logic you want to share between the models.
Cheers.

Rails Active Record Nested Attributes Validation which are in the same request

I have two models house and booking.Everything is okey over booking_date validation. But when I try to update or create multi booking in the same request. Validation can't check the invalid booking in the same request params.
Let give an example assume that booking table is empty.
params = { :house => {
:title => 'joe', :booking_attributes => [
{ :start_date => '2012-01-01', :finish_date => '2012-01-30 },
{ :start_date => '2012-01-15', :finish_date => '2012-02-15 }
]
}}
Second booking also save but its start_date is between first booking interval. When I save them one by one validation works.
class House < ActiveRecord::Base
attr_accessible :title, :booking_attributes
has_many :booking
accepts_nested_attributes_for :booking, reject_if: :all_blank, allow_destroy: true
end
class Booking < ActiveRecord::Base
belongs_to :house
attr_accessible :start_date, :finish_date
validate :booking_date
def booking_date
# Validate start_date
if Booking.where('start_date <= ? AND finish_date >= ? AND house_id = ?',
self.start_date, self.start_date, self.house_id).exists?
errors.add(:start_date, 'There is an other booking for this interval')
end
# Validate finish_date
if Booking.where('start_date <= ? AND finish_date >= ? AND house_id = ?',
self.finish_date, self.finish_date, self.house_id).exists?
errors.add(:finish_date, 'There is an other booking for this interval')
end
end
end
I google nearly 2 hours and could not find anything. What is the best approach to solve this problem?
Some resources
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
http://railscasts.com/episodes/196-nested-model-form-part-1
This was only a quick 15-minutes research on my part, so I may be wrong, but I believe here's the root cause of your problem:
What accepts_nested_attributes_for does under the hood, it calls 'build' for new Booking objects (nothing is validated at this point, objects are created in memory, not stored to db) and registers validation and save hooks to be called when the parent object (House) is saved. So, in my understanding, all validations are first called for all created objects (by calling 'valid?' for each of them. Then, again if I get it right, they are saved using insert_record(record,false) which leads to save(:validate => false), so validations are not called for the 2nd time.
You can look at the sources inside these pages: http://apidock.com/rails/v3.2.8/ActiveRecord/AutosaveAssociation/save_collection_association,
http://apidock.com/rails/ActiveRecord/Associations/HasAndBelongsToManyAssociation/insert_record
You validations call Booking.where(...) to find the overlapping dates-ranges. At this point the newly created Booking objects are still only in memory, not saved to the db (remember, we are just calling valid? for each of them in the loop, saves will be done later). Thus Booking.where(...) which runs a query against a DB doesn't find them there and returns nothing. Thus they all pass valid? stage and then saved.
In a nutshell, the records created together in such a way will not be cross-validated against each other (only against the previously existing records in the database). Hence the problem you see.
Thus either save them one-by-one, or check for such date-overlapping cases among the simultaneously created Bookings yourself before saving.

In Rails 3 how can you access the user_id of the last user in the database in your model?

Edit See Below:
class Post < ActiveRecord::Base
last_user_id = User.last.id
validates_inclusion_of :user_id, :in => 0..last_user_id
end
The above solution works but as vojlo explains, once in production the code will only be executed once and the model will then validate against an incorrect range of users.
I'm working on a tutorial (rails 3.0.3) and have tried for the last half hour to figure out how to tell rails that one of the classes in my model should make sure the :user_id is within the range zero to the user_id of the last user in the database.
I know I need to be using:
validates_inclusion_of :user_id, :in 0..(can't figure out this piece)
I was able to easily ensure the number entered for User_ID in numeric with:
validates_numericality_of :user_id
I'm looking for information on where to research this, I took a good look at the ActiveRecord Validators documentation and didn't find much there.
What happens if a User deletes their account? Don't you really just want to check for existence of the user?
class Post < ActiveRecord::Base
belongs_to :user
validates_presence_of :user
...
Otherwise it's this pattern, which evaluates the post for validity against your requirements - the user_id is greater than zero and less than the largest user ID currently in the database:
class Post < ActiveRecord::Base
include ActiveModel::Validations
validates_with UserIdValidator
...
class UserIdValidator < ActiveModel::Validator
def validate(record)
max_user = User.find(:one, :order=>["id"])
unless(user_id > max_user.id && user_id > 0)
record.errors[:base] << "This record is invalid"
end
end
end
But I still don't quite understand why you would want to do this - is there something particularly special about your user id? I'd recommend the first approach.
Range from zero to last user id? The only way is writing a new validator.
I don't understand your purpose, but check out validates_associated.
Since you're just doing this for learnings sake:
(A)
Check for "last id" presuming it is the "largest id value":
class Post < ActiveRecord::Base
last_user_id = ActiveRecord::Base.connection.select_one('SELECT MAX(ID) AS "MAX_ID" FROM users')["MAX_ID"]
validates_inclusion_of :user_id, :in => 0..last_user_id
end
or
(B) MAX() will get the max value of the ID field. This may not necessarily be the "latest" record inserted. If you really want to get the "last user inserted" you could do this (checks for the latest time of insertion of the record):
class Post < ActiveRecord::Base
last_user_id = ActiveRecord::Base.connection.select_one('SELECT ID AS "LAST_ID" FROM users WHERE created_at = (SELECT MAX(created_at) from users LIMIT1)')["LAST_ID"]
validates_inclusion_of :user_id, :in => 0..last_user_id
end
Why not use validates_uniquness_of :user_id?
Assuming that the range of 0..the_latest_user_id contains all existing user ids, then you are simply looking to see if it is unique. Determining its uniqueness would be the same thing as determining if it is in that range.

Resources