Rails calculate and set a field value in the controller - ruby-on-rails

I have an event table that contains start_at and ends_at. It also have a boolean for allday.
If the event is allday, then the user enters a field called hours. But, if they enter the start_at and ends_at, I'ld like to calculate and save in the hours field.
In English (not Ruby):
If event.allday = > true
hours = ends_at - starts_at
This way, I wouldn't have to calculate it during all the different display pages I have and it would be easier to sum.
OR - would this logic go into the model?
Thanks!
UPDATE-----------
I tried putting this into the model:
before_save :calculate_hours
def calculate_hours
if self.allday != true
self.hours = ((self.ends_at - self.starts_at)/ 3600).to_f.round(2)
end
end

Business logic always belongs in the model.
class Event < ActiveRecord::Base
before_validation :set_hours
private
def set_hours
if allday?
self.hours = ends_at - starts_at
end
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

ActiveRecord: Get all users who should be banned today

I have two models:
class User < ActiveRecord::Base
has_many :ban_messages, dependent: :destroy
end
class BanMessage < ActiveRecord::Base
belongs_to :user
end
Table BanMessage contains a field t.datetime :ban_date that stores the date, when user should be banned. User table contains a field status, that contains one of the status values (active, suspended, banned). When I send BanMessage to User, his status field changes from 'active' to 'suspended'. Also I can activate user back, so he would not be banned at ':ban_date', but his BanMessage wouldn't be destroyed.
So I need to create query for selecting all Users with status 'suspended', who have in their newest BanMessages records 'ban_date' field, with DateTime value, which is between 'DateTime.now.beginning_of_day' and 'DateTime.now.end_of_day'.
You can filter your users with where query followed by join method for associated ban_messages with where method to further filter with the date range.
User.where(status: 'suspended').joins(:ban_messages).where("ban_date >= ? AND ban_date <= ?", DateTime.now.beginning_of_day, DateTime.now.end_of_day )
Try something like this.
User.joins(:ban_messages).where(status: :suspended).where('ban_messages.ban_date BETWEEN :begin_time AND :end_date', begin_time: DateTime.now.beginning_of_day, end_time: DateTime.now.end_of_day)
Here's what I needed:
User.joins(:ban_messages)
.where('users.status = ?', :suspended)
.where('ban_messages.created_at = (SELECT MAX(ban_messages.created_at) FROM ban_messages WHERE ban_messages.user_id = users.id)')
.where('ban_messages.ban_date BETWEEN ? and ?', DateTime.now.beginning_of_day, DateTime.now.end_of_day)
.group('users.id')
UPDATE
MrYoshiji: The caveat in your code is that if you miss one day, then
the next day you won't see the people that should have been banned the
day before. You might want to get all User where the ban_date is
lesser than DateTime.now.end_of_day, either in the same view or
another one.
So final query might be something like this
User.joins(:ban_messages)
.where('users.status = ?', :suspended)
.where('ban_messages.created_at = (SELECT MAX(ban_messages.created_at) FROM ban_messages WHERE ban_messages.user_id = users.id)')
.where('ban_messages.ban_date < ?', DateTime.now.end_of_day)
.group('users.id')

Rails 4 ActiveRecord scope to query different date field based on whether it exists

I have a VoyageLeg model that has many date fields. I have an instance method as below to get one of the dates based on whether it has been populated:
def calc_arrival_date
if ada
ada #return actual date of arrival
elsif updated_eda
updated_eda #return updated estimated date of arrival
else
eda #return estimated date of arrival
end
end
I also have a Voyage model that has_many voyage_legs. How would I create a scope on the Voyage model would would reference the calc_arrival_date, or alternatively do the same logic regarding which date to query in the scope?
I.e. I would like to be able to have a scope on the Voyage that would do something like:
scope :archived, -> { joins(:voyage_legs).where("voyage_legs.calc_arrival_date < ?", Time.zone.now) }
To achieve your request, a little more extra work to be done.
The scope method,
scope :archived, -> {joins(:voyage_legs).select('(CASE WHEN voyage_legs.ada IS NOT NULL THEN voyage_legs.ada WHEN voyage_legs.updated_eda IS NOT NULL THEN voyage_legs.updated_eda WHEN voyage_legs.ada IS NULL and voyage_legs.updated_eda IS NULL THEN voyage_legs.eda END) as calc_arrival_date')}
And a little calculations on the result,
result = Voyage.archived.select { |b| b.calc_arrival_date.to_datetime < Time.zone.now }
I ended up using part of your code #Shabini Rajadas, but combined the date clause inside the logic as a class method on the VoyageLeg model as follows:
def self.departed
where("CASE WHEN voyage_legs.add IS NOT NULL THEN
voyage_legs.add < :now
WHEN voyage_legs.updated_edd IS NOT NULL THEN
voyage_legs.updated_edd < :now
WHEN voyage_legs.add IS NULL and voyage_legs.updated_edd IS NULL THEN
voyage_legs.edd < :now END", now: Time.zone.now.to_date)
end
Then I can filter Voyages like Voyage.joins(:voyage_legs).merge(VoyageLeg.departed)

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

Record Expiration

My Rails app allows administrators to give other players infractions, which are stored in a database table. These infractions have point values that add up to give the player a points value. However, these records will contain an expiry time stamp. Is there any way of automatically deleting the infraction record when the expiry date has been reached?
How about using a default scope that filters out expired records? Something like
class Infraction < ActiveRecord::Base
default_scope -> { where("expired_at IS NULL OR expired_at < ?", Time.now) }
before_create :set_expired_at
private
def set_expired_at
self.expired_at = Time.now + 1.week
end
end
This way, you can do Infraction.find(4), and if the infraction with id == 4 is expired, it won't be returned.

Resources