Rails validation / scope on 24 hour period - ruby-on-rails

I'm trying to put a validation on a record. The validation will check that the record can't be created if the ip_address and post_id are the same. This works good.
I am trying to add another condition that will allow this duplication only if it is after a 24 hour period, if so, allow it to save, but all future saves will be disabled again until the 24 period is over.
Here has been my best attempt so far:
validates_uniqueness_of :ip_address, scope: :impressionable_id,
conditions: -> {where('created_at < ?', Time.now - 24.hours)}
So the validation should somehow only check the "latest" record of the group it finds to do the validation to be accurate
Thank you!

It might be easier to just make an explicit validator method for this:
class Foo < ActiveRecord::Base
validate :ip_address_uniqueness, on: :create
# ...
def ip_address_uniqueness
existing = Foo.where(impressionable_id: self.impressionable_id)
.where(ip_address: self.ip_address)
.where(created_at: Time.current.all_day)
errors.add(:ip_address, 'cannot be used again today') if existing
end
end

Related

Custom validation before create in rails

I have a before_create function which i am using to populate a column in the table . For instance
before_create :generate_number
def generate_number
end
I want to check the uniqueness of the column before its populated based on a condition .
example : if the number is created in database 2years ago i dont want the uniqueness validation to be applied on it
how could i write a custom validation for it. And where i should include it?
You can use unless or if to apply logic to a validation:
validate_uniqueness_of :column_name, unless: -> { created_at < 2.years.ago }
This would go in your ActiveRecord model in the same manner as your before_create
class User < ActiveRecord::Base
#...
validate_uniqueness_of :column_name, unless: -> { created_at < 2.years.ago }
end
Since you specified in a comment that you'd like for it to continue generating numbers until one is generated that is not already in the database:
def generate_number
begin
value = generate
end while created_at < 2.years.ago && Table.exists?(column: value)
self.column = value
end
def generate
... # Code to generate number here
end
This continues looping while the created_at column is sooner than 2 years ago and the number generated is in the database. I put the while conditional at the bottom so that generate will run at least once before being checked.
I still don't understand the purpose of the created_at conditional, I can modify that part if you would like to specify how it should work. Otherwise, I'll just consider it an example, and not part of the actual issue.

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 race condition uniqueness intersection dates

I am having trouble figuring out how to solve race condition in this scenario.
I am using rails 3.2.11 with active record
I have the models: (just an example)
class Task < ActiveRecord::Base
belongs_to :person
attr_accessible :start_date, :end_date, :person_id
end
class Person < ActiveRecord::Base
has_many :tasks
end
The problem is:
I want to ensure that one person only have 1 task assigned between that interval (start_date and end_date)
For example,
Task | Person_id | start_date | end_date
0 1 01-01-01 10:00 01-01-01 11:00
1 1 01-01-01 10:15 01-01-01 10:45 ## this should no be valid!!
I know that validations in Rails don't prevent this because of race conditions, and I cannot add index at database level because I dont want the unique => true but instead some sort of unique_in_interval => true
Could someone explain how to accomplish this?
Thanks in advance.
Just to give you the idea, use after_create callback to check the uniqueness of the field just inserted during that conditional interval.
So in your tasks model,
attr_accessor :between_date
after_create :discard_insert
def discard_insert
#same_records = Person.joins(:tasks).where(:tasks=> {:between_date => start_date..end_date})
end
The above method will give you all the records with the race conditions and than you can delete the later one.
Just lock the operation using any locking mechanism like redis locker
RedisLocker.new("assign_task_#{#user.id}").run! do
#user.assign_task(#task) unless #user.has_task_within_time_period?
end

Associated model validations in Rails - and some questions of design

I've got a Task which has many LoggedTimes. I want to put a limit of 8 hours of logged time against a task on any given day. What is the best way to go about this so that I can check whether the person's latest logged time "takes their total over the limit" for a day, so to speak?
Here's where I am beginning (Task.rb):
validate :max_logged_daily_time
def max_logged_daily_time
if (params[:session_time] + (logged_times.where(:created_at => Date.today).to_a.sum(&:session_time)/60)) > 8
errors.add_to_base("Can't have more than 8 hours logged a day")
logged_time.errors.add('session_time', 'Logged times exceeded today')
end
end
Currently this validation is not working (adding another LoggedTime once 8 hours of previous logged times have been registered simply adds it to the rest, instead of throwing an error. Since no errors are being thrown, I'm struggling to pick my way through the problem. Is it something to do with the handling of params?
Which brings me to the question of design: in theory I could revise the view so that the user is only able to submit 8 hours minus the total amount of time they've logged that day; however this seems like a clunky solution and against the principle of keeping validations in the model. (And it certainly doesn't help me solve this model validations problem).
Any advice here?
TIA
class Task < ActiveRecord::Base
has_many :logged_times
def hours_today
LoggedTime.daily_hours_by_task(self).to_a.sum(&:session_time)
end
end
class LoggedTime < ActiveRecord::Base
belongs_to :task
scope :daily_hours_by_task, lambda { |task| task.\
logged_times.\
where('logged_times.created_at >= ? AND logged_times.created_at < ?',
Date.today, Date.today + 1) }
validate :max_logged_daily_time
private
def max_logged_daily_time
if task && ((task.hours_today + session_time) / 60.0) > 8
errors.add('session_time', 'Logged times exceeded today')
end
end
end
Some notes:
created_at is a DateTime, so you'll
need to test both the start and end
of the day
The validation also prevents the
addition of a single LoggedTime which by itself exceeds the maximum.
Dividing by an integer truncates and
will give the wrong result -- add the
.0 to convert to a float.
This only validates the LoggedTime,
not the Task, so you might want to
add validates_associated in the
Task model.
Validation is bypassed when Task is nil
EDIT
Well, hours_today should really be called minutes_today, but you get the idea.
I'd create a separate method for getting the sum of hours for a given day.
def total_hrs_logged_for_date(date)
#some code
end
Test that method to make sure it works.
Possibly also do the same for calculating the current logged time.
then use those those two in your custom validator
so this line
if (params[:session_time] + (logged_times.where(:created_at => Date.today).to_a.sum(&:session_time)/60)) > 8
becomes
if total_hrs_logged_for_date(Date.today) + current_time_being_logged > 8
At the very least it will help you narrow down which of these isn't working.
I also notice that you have "params[:session_time]"
I think this is in Task.rb which sounds like a model. Probably what you want is just "session_time" instead.
I ended up revising Zetetic's response slightly, because I couldn't get it to work as is.
In the end, this worked:
class Task < ActiveRecord::Base
has_many :logged_times
validates_associated :logged_times
def minutes_today
logged_times.where('created_at >= ? AND created_at < ?', Date.today, Date.today + 1)
end
end
and the LoggedTime model:
class LoggedTime < ActiveRecord::Base
belongs_to :task
validate :max_logged_daily_time
private
def max_logged_daily_time
if task && ((task.minutes_today + session_time) / 60.0) > 8
errors.add('session_time', 'Logged times exceeded today')
end
end
end
I'm not sure why the scope method baulked, but it did. Any hints Zetetic?

Custom Rails Validation - multiple fields simultaneously

I'm looking to create a custom validation in Rails. I need to validate that a POSTed start_date_time and end_date_time (together) do not overlap that combination in the database.
Example:
In the database:
start_date
05/15/2000
end_date
05/30/2000
POSTed:
start_date
05/10/2000
end_date
05/20/2000
FAILS!
Here's the rub:
1) I want to send both start and end fields into the function
2) I want to get the values of both POSTed fields to use in building a query.
3) Bonus: I want to use a scope (like say, for a given [:user_id, :event] -- but, again, I want that to be passed in.
How do I get the values of the fields?
Let's say my function looks like this:
def self.validates_datetime_not_overlapping(start, finish, scope_attr=[], conf={})
config = {
:message => 'some default message'
}
config.update(conf)
# Now what?
end
I'm sort of stuck at this point. I've scoured the net, and can't figure it out.... I can get the value of either start or finish, but not both at the same time by using validate_each ...
Any help would be great!
Thanks :)
What about custom validation methods?
You can add:
validate :check_dates
def check_dates
do.whatever.you.want.with.any.field
end
EDIT:
So maybe validate_with?
Modified example from RoR Guides:
class Person < ActiveRecord::Base
validates_with DatesValidator, :start => :your_start_date, :stop => :your_stop_date
end
class DatesValidator < ActiveRecord::Validator
def validate
start = record.send(options[:start])
stop = record.send(options[:stop])
...
end
end

Resources