Check for Dates Overlap in Ruby on Rails - sql

I have a model that has two columns (started_at and ended_at). I want to add a custom validator that ensures that no other record exists with dates that overlap with the record I'm validating. So far I have:
# app/models/event.rb
class Event < ActiveRecord::Base
validates_with EventValidator
end
# app/validators/event_validator.rb
class EventValidator < ActiveModel::Validator
attr_reader :record
def validate(record)
#record = record
validate_dates
end
private
def validate_dates
started_at = record.started_at
ended_at = record.ended_at
arel_table = record.class.arel_table
# This is where I'm not quite sure what type of query I need to perform...
constraints = arel_table[:started_at].gteq(ended_at)
.and(arel_table[:ended_at].lteq(started_at))
if record.persisted?
constraints = constraints
.and(arel_table[:id].not_eq(record.id))
end
if record.class.where(constraints).exists?
record.error[:base] << "Overlaps with another event"
end
end
end
I don't know exactly what query I need to ensurethat there is no overlapping. Any help is greatly appreciated

I don't use Arel but I think the query should be:
constraints = arel_table[:started_at].lteq(ended_at)
.and(arel_table[:ended_at].gteq(started_at))
Two periods overlap when
period1.start < period2.end
period1.end > period2.start

Have a look at Validates Overlap gem
You can either use it instead your code or take condition code from it
# Return the condition string depend on exclude_edges option.
def condition_string(starts_at_attr, ends_at_attr)
except_option = Array(options[:exclude_edges]).map(&:to_s)
starts_at_sign = except_option.include?(starts_at_attr.to_s.split(".").last) ? "<" : "<="
ends_at_sign = except_option.include?(ends_at_attr.to_s.split(".").last) ? ">" : ">="
query = []
query << "(#{ends_at_attr} IS NULL OR #{ends_at_attr} #{ends_at_sign} :starts_at_value)"
query << "(#{starts_at_attr} IS NULL OR #{starts_at_attr} #{starts_at_sign} :ends_at_value)"
query.join(" AND ")
end

I would construct a query that looks something like this:
Event.exists?( 'started_at < ? AND ended_at > ?', ended_at, started_at )
If this returns true, an overlapping record exists.

Related

Writing a SQL Server query in a Rails controller

I am trying to execute a raw SQL query in my rails controller. I tested the query a database program and it works.
Here is my controller (sites_controller.rb):
class SitesController < ApplicationController
def index
#sites = SELECT * FROM "AM-Martin".dbo.CpCore_Site WHERE "Number" LIKE 'LA%' OR "Number" LIKE 'LC%'
#hash = Gmaps4rails.build_markers(#sites) do |site, marker|
marker.lat site.latitude
marker.lng site.longitude
end
end
end
I keep getting this long error message that it is not liking the way I wrote it.
Can anyone help guide me please?
UPDATE
My controller has changed to:
class SitesController < ApplicationController
def index
query = "SELECT * FROM dbo.CpCore_Site WHERE 'Number' LIKE 'LA%' OR 'Number' LIKE 'LC%'"
#sites = ActiveRecord::Base.connection.execute(query)
#hash = Gmaps4rails.build_markers(#sites) do |site, marker|
marker.lat site.latitude
marker.lng site.longitude
end
end
end
But now I am getting an error for the site.latitude part which says:
undefined method `latitude' for 0:Fixnum
latitude is one of columns from the database table that I am trying to extract.
In rails you can't fire query like this. you can do like this
query = "SELECT * FROM "AM-Martin".dbo.CpCore_Site WHERE "Number" LIKE 'LA%' OR "Number" LIKE 'LC%"
#sites = ActiveRecord::Base.connection.execute(query)
Or
#sites = Model.where("Number LIKE 'LA%' OR Number LIKE 'LC%'")
Hope this will help you.

undefined method `order' for []:Array

I have the following code where I load the activities for the user based on whether or not they are an admin. Users can only see their own activity feed, whereas admins can see the entire stream. This is a precursor to sharing activity feeds with friends, etc.
def index
if current_user?
#incidents = Incident.find_all_by_user_id(current_user.id).order("created_at desc")
else
#incidents = Incident.all.order("created_at desc")
end
end
I am getting the above referenced error(undefined method "order" for []:Array). It seems to be the .order reference, but I have checked the rails Guides and it seems to be correct syntax.
Any ideas?
Try changing the find_by... to where, so:
def index
if current_user?
#incidents = Incident.where(user_id: current_user.id).order("created_at desc")
else
#incidents = Incident.all.order("created_at desc")
end
end
should work :-)
The #index action method can be simplified and optimized (by replacement find_all_by with where) to:
def index
clause = current_user? && Incident.where(user_id: current_user.id) || Incident
#incidents = clause.order("created_at desc")
end

Rails ActiveRecord where clause

I want to select Cars from database with where clause looking for best DRY approach for my issue.
for example I have this two parameters
params[:car_model_id] (int)
params[:transmission_id] (int)
params[:from_date]
params[:to_date]
but I dont know which one will be null
if params[:car_model_id].nil? && !params[:transmission_id].nil?
if params[:from_date].nil? && params[:from_date].nil?
return Car.where(:transmission_id => params[:transmission_id])
else
return Car.where(:transmission_id => params[:transmission_id], :date => params[:from_date]..params[:to_date])
end
elseif !params[:car_model_id].nil? && params[:transmission_id].nil?
if params[:from_date].nil? && params[:from_date].nil?
return Car.where(:car_model_id=> params[:car_model_id])
else
return Car.where(:car_model_id=> params[:car_model_id], :date => params[:from_date]..params[:to_date])
end
else
return Car.where(:car_model_id=> params[:car_model_id], :transmission_id => params[:transmission_id], :date => params[:from_date]..params[:to_date])
end
what is best approach to avoid such bad code and check if parameter is nil inline(in where)
You can do:
car_params = params.slice(:car_model_id, :transmission_id).reject{|k, v| v.nil? }
and then:
Car.where(car_params)
Explanation: Since, you're checking if the particular key i.e.: :car_model_id and transmission_id exists in params. The above code would be something like this when you have just :transimission_id in params:
Car.where(:transmission_id => '1')
or this when you have :car_model_id in params:
Car.where(:car_model_id => '3')
or this when you'll have both:
Car.where(:transmission_id => '1', :car_model_id => '3')
NOTE: This will work only when you have params keys as the column names for which you're trying to run queries for. If you intend to have a different key in params which doesn't match with the column name then I'd suggest you change it's key to the column name in controller itself before slice.
UPDATE: Since, OP has edited his question and introduced more if.. else conditions now. One way to go about solving that and to always keep one thing in mind is to have your user_params correct values for which you want to run your queries on the model class, here it's Car. So, in this case:
car_params = params.slice(:car_model_id, :transmission_id).reject{|k, v| v.nil? }
if params[:from_date].present? && params[:from_date].present?
car_params.merge!(date: params[:from_date]..params[:to_date])
end
and then:
Car.where(car_params)
what is best approach to avoid such bad code and check if parameter is
nil inline(in where)
Good Question !
I will make implementation with two extra boolean variables (transmission_id_is_valid and
car_model_id_is_valid)
transmission_id_is_valid = params[:car_model_id].nil? && !params[:transmission_id].nil?
car_model_id_is_valid = !params[:car_model_id].nil? && params[:transmission_id].nil?
if transmission_id_is_valid
return Car.where(:transmission_id => params[:transmission_id])
elseif car_model_id_is_valid
return Car.where(:car_model_id=> params[:car_model_id])
....
end
I think now is more human readable.
First, I would change this code to Car model, and I think there is no need to check if params doesn't exists.
# using Rails 4 methods
class Car < ActiveRecord::Base
def self.find_by_transmission_id_or_model_id(trasmission_id, model_id)
if transmission_id
find_by trasmission_id: trasmission_id
elsif model_id
find_by model_id: model_id
end
end
end
In controller:
def action
car = Car.find_by_transmission_id_or_model_id params[:trasmission_id], params[:car_model_id]
end
edit:
This code is fine while you have only two parameters. For many conditional parameters, look at ransack gem.

Rails: change column name and not value in where clause

How could I change the column name in the argument of a where clause? Something like this is what I'm after:
def self.filter_by_time(datetime)
where('? < ?', datetime, Time.now)
end
Thanks!
def self.filter_by_time(datetime)
where("#{datetime} < ?", Time.now)
end
You can try this:
def self.filter_by_time(datetime)
where("datetime < ?", Time.now)
end

pulling one field of a record into an array in a Rails 3 app

Rails 3 noob here. Currently the code in my controller below is getting the whole record in my database. I am trying to populate the array with one integer, not the whole record. The integer is contained in a table "answers" and the field name is "score". How can i modify this line in my controller to get just the one field?
#questions.each do |s|
#ans[s.position] = Answer.where("question_id = ? AND user_id = ?", s.q_id, current_user.id )
end
UPDATE FOR CLARIFICATION: The :score can be any integer from 0 to 5. I would like to populate #ans[s.position] with the integer.
Thanks.
You're very close
#questions.each do |s|
#ans[s.position] = Answer.where("question_id = ? and user_id = ?",s.q_id,current_user.id).select(:score).first.try(:score)
end
You need to select "score" from Answer, then you need to retrieve it from the object.
Since where could potentially return many Answers, use first to pull off the first Answer, and then score to project out the score field.
answers = Answer.where("question_id = ? AND user_id = ?", s.q_id, current_user.id )
score = answers.first.score
All together it would be:
#questions.each do |s|
#ans[s.position] = Answer.where("question_id = ? AND user_id = ?", s.q_id, current_user.id ).first.score
end
As an optimization to only retrieve score from the database, instead of answers.*, you could use select(:score).
#questions.each do |s|
#ans[s.position] = Answer.where("question_id = ? AND user_id = ?", s.q_id, current_user.id ).select(:score).first.score
end
See Active Record Query Interface for more about using select.
Just include them.
#questions = Question.includes(:answers).select(:position)
Or use this
#questions = Question.select(:position)
#questions.each{ |s| #ans[s.position] = s.answers.where(:user_id => current_user.id) }