Associated model validations in Rails - and some questions of design - ruby-on-rails

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?

Related

How to check if associated model has entries in Rails 5?

I have a model RegularOpeningHour(dayOfWeek: integer) that is associated to a model OpeningTime(opens: time, closes: time). RegularOpeningHour has an 1:n relation to OpeningTime, so that a specific day can have many opening times.
(I know that I simply could have one entry with 'opens' and 'closes' included in RegularOpeningHour but for other reasons I need this splitting)
Now I want a open?-Method, that returns whether the business is opened or not. I tried the following in my model file regular_opening_hour.rb:
def open?
RegularOpeningHour.where(dayOfWeek: Time.zone.now.wday).any? { |opening_hour| opening_hour.opening_times.where('? BETWEEN opens AND closes', Time.zone.now).any? }
end
Unforutnately, that doesn't work. Any ideas to solve this?
How about this:
def open?
joins(:opening_times)
.where(dayOfWeek: Time.current.wday)
.where("opens <= :time AND closes >= :time", time: Time.current)
.any?
end
EDIT: Missing ':' in the join
You could create some scopes to make selecting open OpeningTimes and open RegularOpeningHours less clunky. This makes creating the given selection much easier.
class OpeningTime < ApplicationRecord
# ...
belongs_to :regular_opening_hour
def self.open
time = Time.current
where(arel_table[:opens].lteq(time).and(arel_table[:closes].gteq(time)))
end
# ...
end
class RegularOpeningHour < ApplicationRecord
# ...
has_many :opening_times
def self.open
where(
dayOfWeek: Time.current.wday,
id: OpeningTime.select(:regular_opening_hour_id).open,
)
end
# ...
end
def open?
RegularOpeningHour.open.any?
end
Since you have has_many association of RegularOpeningHour to OpeningTime you can use join query like below.:
RegularOpeningHour.joins(:opening_times).where(dayOfWeek: Time.zone.now.wday).where('? BETWEEN opening_times.opens AND opening_times.closes', Time.zone.now).any?

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 validation / scope on 24 hour period

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

rails3 user can only vote once per day

class Rate < ActiveRecord::Base
attr_accessible :image_id, :rate, :user_id
belongs_to :image
belongs_to :user
validate :user_can_rate_after_one_day
before_save :default_values
def default_values
self.rate ||=0
end
protected
def user_can_rate_after_one_day
r=Rate.where(:image_id =>image_id, :user_id=> user_id).order("created_at DESC").limit(1)
if( (Time.now - 1.day) < r[0].created_at)
self.errors.add(:rate,"you can only vote once per day")
else
return
end
end
end
I have one rate model, and i want the user can only rate once per day. i write the user_can_rate_after_one_day method to validte it. If i delete the function, the user can rate many time, if i add this function, user can not rate it. Anyone knows what's wrong here? Thanks
I have also the same type of problem. I tried to create an application in which a particular user can add standup for the current day only. I wrote the code in my controller create action. The logic is almost similar to your question, but I found one issue in your code due to which your date is not compared properly Time.now - 1.day return in this format 2018-01-22 11:24:23 +0545 and standup[0].created_at return in this format Mon, 22 Jan 2018 08:58:57 UTC +00:00.
So, to solve this issue, I converted both the date in same format i.e
(Time.now - 1.day).strftime('%F %H:%M:%S') < standup[0].created_at.strftime('%F %H:%M:%S') which returns true, and the code works.
It might be helpful to other viewers as well.

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