How to prevent validation of self object - ruby-on-rails

Hi I have a method that check overlapping for new and updated objects.
Method:
def timeline
if start_at_changed? || end_at_changed? || person_id_changed?
if (person.vacations.where('start_at <= ?', start_at).count > 0 &&
person.vacations.where('end_at >= ?', end_at).count > 0) ||
(person.vacations.where('start_at <= ?', start_at).count > 0 &&
person.vacations.where('end_at <= ?', end_at).count > 0 &&
person.vacations.where('end_at >= ?', start_at).count > 0) ||
(person.vacations.where('start_at >= ?', start_at).count > 0 &&
person.vacations.where('start_at <= ?', end_at).count > 0 &&
person.vacations.where('end_at >= ?', end_at).count > 0)
errors.add(:base, 'You have a vacation during this period.')
end
end
end
My issue is while I try edit some vacation. If I change e.g. start_at validation checks also this object and return error. Example: start_at 2016-06-27, end_at: 2016-06-30. I try change start_at to 2016-06-26. Validation returns 'You have a vacation during this period.', because it check period 2016-06-27 - 2016-06-30. How to exclude checking object that I try update, only other?

You just need to exclude the current vacation id on you queries. Something like:
def timeline
if start_at_changed? || end_at_changed? || person_id_changed?
if (person.vacations.where('start_at <= ? and vacations.id != ?', start_at, id).count > 0 &&
person.vacations.where('end_at >= ? and vacations.id != ?', end_at, id).count > 0) ||
...
end
end
end
Also you can use the not condition. More info here and here.
Just to point, you're executing a lot of queries here. This can be very expensive in production. My suggestion is to merge all this queries in just one query, so besides the speed improvements, the readability of your code will be really better.
You code will look like this:
def timeline
if start_at_changed? || end_at_changed? || person_id_changed?
overlapping_vacations = person.vacations.where('vacations.id != ?', id)
overlapping_vacations = overlapping_vacations.where('((start_at <= :start_at AND end_at >= :end_at) OR
(start_at <= :start_at AND end_at <= :end_at AND end_at >= :start_at) OR
(start_at >= :start_at AND start_at <= :end_at AND end_at >= :end_at))',
start_at: start_at, end_at: end_at)
if overlapping_vacations.exists?
errors.add(:base, 'You have a vacation during this period.')
end
end
end

Related

ERROR: undefined method `year' for nil:NilClass

I have a code to calculate the age of a person, which worked perfectly, and it just stopped working.
Model:
def age
now = Time.now.utc.to_date
now.year - fechanacimiento.year - ((now.month > fechanacimiento.month || (now.month == fechanacimiento.month && now.day >= fechanacimiento.day)) ? 0 : 1)
end
My view:
<%= patient.age %>
def age
now = Time.now.utc.to_date
fechanacimiento && now.year - fechanacimiento.year - ((now.month > fechanacimiento.month || (now.month == fechanacimiento.month && now.day >= fechanacimiento.day)) ? 0 : 1)
end
An alternative is to use the database to calculate the age.
Postgres:
patients = Patient.select(
'patients.*',
'AGE(patients.date_of_birth) AS age'
)
MySQL:
patients = Patient.select(
'patients.*',
'TIMESTAMPDIFF(YEAR, date_of_birth, CURDATE()) AS age'
)
The advantage is that you can order and group the records by age if needed.

Better solution for "one, other or both" cases

I was checking some code, and something similar to the following showed up:
def between_dates(date_1, date_2)
if date_1 && date_2
conditions "created_at >= date_1 AND created_at <= date_2"
elseif date_1
conditions "created_at >= date_1"
elseif date_2
conditions "created_at <= date_2"
end
end
It looked the kind of code that could be improved, but I couldn't find a more elegant solution for such a trivial and common conditional statement.
I'm looking for a better answer for this problem when we must return a value for one, other or both.
Rails lets you build a query dynamically. Here's an example using scopes and a class method. Since scopes always return an ActiveRecord::Relation object (even if the block returns nil), they are chainable:
class Event < ApplicationRecord
scope :created_before, -> (date) { where('created_at <= ?', date) if date }
scope :created_after, -> (date) { where('created_at >= ?', date) if date }
def self.created_between(date_1, date_2)
created_after(date_1).created_before(date_2)
end
end
Example usage:
Event.created_between(nil, Date.today)
# SELECT `events`.* FROM `events` WHERE (created_at <= '2018-05-15')
Event.created_between(Date.yesterday, nil)
# SELECT `events`.* FROM `events` WHERE (created_at >= '2018-05-14')
Event.created_between(Date.yesterday, Date.today)
# SELECT `events`.* FROM `events` WHERE (created_at >= '2018-05-14') AND (created_at <= '2018-05-15')
I'd use something like this:
def between_dates(date_1, date_2)
parts = []
if date_1
parts << "created_at >= date_1"
end
if date_2
parts << "created_at <= date_2"
end
full = parts.join(' AND ')
conditions(full)
end
This can be further prettified in many ways, but you get the idea.
def between_dates(date_1, date_2)
date_conditions = []
date_conditions << 'created_at >= date_1' if date_1
date_conditions << 'created_at <= date_2' if date_2
conditions date_conditions.join(' AND ') unless date_conditions.empty?
end
I am not sure if this is more elegant, but I always do reduce everything to avoid typos:
[[date_1, '>='], [date_2, '<=']].
select(&:first).
map { |date, sign| "created_at #{sign} #{date}" }.
join(' AND ')

Rails scope with combined attributes

I am trying to create a scope where checkin_time (DateTime) is added with duration (Int) in hours which is greater than or equal to Time.now
So something like this
scope :current, -> { where('checkin_time + duration.hours >= ?', Time.now) }
However, the string 'checkin_time + duration.hours >= ?' above is not valid. How can I achieve something that will give me the correct result?
The easiest thing to do with PostgreSQL is to convert your duration to an interval and add that interval to your timestamp. Something like this:
where("checkin_time + (duration || 'hours')::interval >= ?", Time.now)
or even:
where("checkin_time + (duration || 'hours')::interval >= now()")
The duration || 'hours' builds a string like '6hours' and then the ::interval casts that string to an interval which can be added to a timestamp.

Ruby on Rails, Select Users with age range from Active Record

User.rb
# Attributes
# (..)
# birthdate (string)
# format "mm/yyyy"
def age
dob = self.birthdate.to_date
now = Time.now.utc.to_date
now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
end
In the console:
irb(main):002:0> current_user.age
=> 7
I'd be able to do the following:
age_range = "25-65"
User.where(:age => between age_range)
I'm stuck at the point how to get the value from age (class method) into the where call
First of all: use a date as type for birthdate in the database.
Then you can just use:
User.where(birthdate: 65.years.ago..25.years.ago)
If you can't change the birthdate type convert it using SQL (example with PostrgeSQL):
User.where('to_date(birthdate, 'MM/YYYY') between ? and ?', 65.years.ago, 25.years.ago)
But you may still have to correct it since you don't have the exact day and only the month.
With PostgreSQL you can use that Rails scope
scope :for_age_range, -> min, max {
where("date_part('year', age(birthdate)) >= ? AND date_part('year', age(birthdate)) <= ?", min, max)
}
User.for_age_range(18, 24)

rails custom date validation

In the form the user has to choose a month and a year. I'd like to make sure that the date is not in the future based on the month. So let's say the current date is 07/01/2016 in which case the user should be able to choose 07/2016 but shouldn't be able to choose 08/2016.
The validation seems to be working, but doesn't look well. Is there an easier way
achieve this?
validate :founded_in_the_past
def founded_in_the_past
if founded && ((founded.month > Date.current.month && founded.year == Date.current.year)
&& founded.year > Date.current.year)
errors.add :base, "Founding date must be in the past."
end
end
Instead of checking with month and year why you are not comparing date directly by creating date object using month and year.
validate :founded_in_the_past
def founded_in_the_past
if founded && (Date.new(founded.year, founded.month, 1) > Date.current)
errors.add :base, "Founding date must be in the past."
end
end
In your code if you select year as 2017 and month as 5 - it will allow to select this date
founded && ((founded.month > Date.current.month &&
founded.year == Date.current.year) &&
founded.year > Date.current.year)
founded = true
founded.month > Date.current.month = false
founded.year == Date.current.year = false
founded.year > Date.current.year = true
If we map this in your condition you condition will look like
true && ((false && false) && true)
This will return false and it will allow user select future date
You can cut out one conditional by using >= eg
if founded && (founded.month > Date.current.month && founded.year >= Date.current.year)

Resources