I created a model: Lecture(start_time, end_time, location). I want to write validation functions to check wether the new lecture's time overlapping with saved lectures in database. So that I can find out if the location is occupied in that time. My function is:
class Lecture < ActiveRecord::Base
validates :title, :position, presence: true
validates :start_time, :end_time, format: { with: /([01][0-9]|2[0-3]):([0-5][0-9])/,
message: "Incorrect time format" }
validate: time_overlap
def time_overlap
Lecture.all.each do |user|
if (user.start_time - end_time) * (start_time - user.end_time) >= 0
errors.add(:Base, "time overlaps")
end
end
end
end
The error message: NoMethodError in LecturesController#create
undefined method `-#' for nil:NilClass. How to write this function in right format?
Take a look at Ruby 2.3.0's Time class: http://ruby-doc.org/core-2.3.0/Time.html
You can use it to check if a Time instance is before or after another Time instance, such as:
t1 = Time.now
t2 = Time.now
t1 < t2
=> true
t1 > t2
=> false
So, to check if a given time would exist during an existing Lecture in the database, you could write some Ruby to check if the proposed Lecture's start time or finish time sits after the start time AND before the end time of any existing Lectures.
Lets say you have two slots of time, as:
start_time_a
end_time_a
start_time_b
end_time_b
There are three possible cases where there can be an overlap between the two time slots.
1) start_time_b >= start_time_a && start_time_b =< end_time_a ( i.e, slot b starts somewhere in the middle of slot a )
2) end_time_b >= start_time_a && end_time_b <= end_time_a ( i.e, slot b ends somewhere between slot a )
3) start_time_b <= start_time_a && end_time_b >= end_time_a ( i.e, slot b is larger than slot a, and completely covers it.
If you check for these three conditions, you can determine if there's an overlap between two time slots.
Conditions 1 & 2 can be simplified using start_time_b.between?(start_time_a, end_time_a).
Related
I am using Rails 4.2rc3 and the simple_form gem.
I have an Event model which has a field reservations_open_at which indicates how many seconds before the event starts that reservations can start to be made (e.g. reservations might open up 2 days in advance, or maybe 10 hours in advance).
However, I want to use a form with a text input for a number (reservations_open_at_amount) and a select form for either 'Hours' or 'Days' (reservations_open_at_unit). I.e. the user can input '2 days' or '10 hours' via the two fields.
I can't quite figure out the right way to use virtual attributes to set the reservations_open_at field in seconds using a regular #event.update(event_params) call in my controller.
I don't need to remember whether the user chose Hours or Days when editing an existing record; if the reservations_open_at amount modulo (24*60*60) is zero (i.e. the number of seconds is an exact multiple of days) then I will display the form with reservations_open_at / (24*60*60) and Days, else reservations_open_at / (60*60) and Hours.
You are already more than half way there.
In the model, add two virtual attributes, time_amount, time_units. Add these 2 to the views.
Add a before_save callback to set the value in reservations_open_at.
Something like:
Edit: add getter for time unit
class Event < ActiveRecord::Base
attr_accessor :time_unit, time_amount
before_save :calculate_open_at
...
def time_unit
return #time_unit if #time_unit
if ( self.time_amount = reservations_open_at / 1.day.seconds ) > 0
#time_unit = 'days'
else
self.time_amount = reservations_open_at / 1.hour.seconds
#time_unit = 'hours'
end
end
def calculate_open_at
conversion_rate = time_unit == "days" ? 1.day.seconds : 1.hour.seconds
self.reservations_open_at = time_amount * conversion_rate
end
...
end
I'm still very new to Rails but moving along fairly smoothly I would say. So for practice I'm working on what's supposed to be a simple application where a user can input their weight and that info, over a 30 day period, is displayed to them via a Highcharts line graph using the Lazy Highcharts gem. I followed Ryan Bates' Railscast #223 to get started.
My Issue:
Ok, so the inputs are showing up except that on the days that a user doesn't input a value it gets displayed on the chart as '0' (the line drops to bottom of the graph), instead of connecting to the next point on any given day. Not sure if all that makes sense so here's a screenshot:
I found this solution:
Highcharts - Rails Array Includes Empty Date Entries
However, when I implement the first option found on that page (convert 0 to null):
(30.days.ago.to_date..Date.today).map { |date| wt = Weight.pounds_on(date).to_f; wt.zero? ? "null" : wt }
the points show up but the line does not, nor do the tooltips...this leads me to think that something is breaking the js. Nothing is apparently wrong in the console though..? From here I thought it might be a matter of using Highchart's 'connectNulls' option but that didn't work either. When implementing the second option (reject method):
(30.days.ago.to_date..Date.today).map { |date| Weight.pounds_on(date).to_f}.reject(&:zero?)
it completely removes all dates that are null from the chart, which messes up the structure completely because the values are supposed to be displayed based on their created_at date.
So back to square one, this is what I'm left with (chart plotting zeros for days without inputs).
Model:
class Weight < ActiveRecord::Base
attr_accessible :notes, :weight_input
validates :weight_input, presence: true
validates :user_id, presence: true
belongs_to :user
def self.pounds_on(date)
where("date(created_at) = ?", date).pluck(:weight_input).last
end
end
Controller:
def index
#weights = current_user.weights.all
#startdate = 30.days.ago.to_date
#pounds = (30.days.ago.to_date..Date.today).map { |date| Weight.pounds_on(date).to_f }
#h = LazyHighCharts::HighChart.new('graph') do |f|
f.options[:title][:text] = " "
f.options[:chart][:defaultSeriesType] = "area"
f.options[:chart][:inverted] = false
f.options[:chart][:zoomType] = 'x'
f.options[:legend][:layout] = "horizontal"
f.options[:legend][:borderWidth] = "0"
f.series(:pointInterval => 1.day, :pointStart => #startdate, :name => 'Weight (lbs)', :color => "#2cc9c5", :data => #pounds )
f.options[:xAxis] = {:minTickInterval => 1, :type => "datetime", :dateTimeLabelFormats => { day: "%b %e"}, :title => { :text => nil }, :labels => { :enabled => true } }
end
respond_to do |format|
format.html # index.html.erb
format.json { render json: #weights }
end
end
Does anyone have a solution for this? I guess I could be going about this all wrong so any help is much appreciated.
This is a HighCharts specfic. You need to pass in the timestamp into your data array vs. defining it for the dataset.
For each data point, I set [time,value] as a tuple.
[ "Date.UTC(#{date.year}, #{date.month}, #{date.day})" , Weight.pounds_on(date).to_f ]
You will still need to remove the zero's in w/e fashion you like, but the data will stay with the proper day value.
I do think you need to remove :pointInterval => 1.day
General tips
You should also, look at optimizing you query for pound_on, as you are doing a DB call per each point on the chart. Do a Weight.where(:created_at => date_start..date_end ).group("Date(created_at)").sum(:weight_input) which will give you an array of the created_at dates with the sum for each day.
ADDITIONS
Improved SQL Query
This leans on sql to do what it does best. First, use where to par down the query to the records you want (in this case past 30 days). Select the fields you need (created_at and weight_input). Then start an inner join that runs a sub_query to group the records by day, selecting the max value of created_at. When they match, it kicks back the greatest (aka last entered) weight input for that given day.
#last_weight_per_day = Weight.where(:created_at => 30.days.ago.beginning_of_day..Time.now.end_of_day)
select("weights.created_at , weights.weight_input").
joins("
inner join (
SELECT weights.weight_input, max(weights.created_at) as max_date
FROM weights
GROUP BY weights.weight_input , date(weights.created_at)
) weights_dates on weights.created_at = weights_dates.max_date
")
With this you should be able #last_weight_per_day like so. This should not have 0 / nil values assuming you have validated them in your DB. And should be pretty quick at it too.
#pounds = #last_weight_per_day.map{|date| date.weight_input.to_f}
I have a validator class that i am writing that has three validations, that are run when calling MyVariableName.valid?
validates_length_of :id_number, :is => 13, :message => "A SA ID has to be 13 digits long"
validates_format_of :id_number, :with => /^[0-9]+$/, :message => "A SA ID cannot have any symbols or letters"
validate :sa_id_validator
The third one is a custom validator. The thing is that my validator sa_id_validator requires that the data that is passed in is a 13 digit number, or I will get errors. How can I make sure that the validate :sa_id_validator is only considered after the first two have run?
Sorry if this is a very simple question I have tried figuring this out all of yesterday afternoon.
Note: this validator has to run over a couple thousand entries and is also run on a spreadsheet upload so I need it to be fast..
I saw a way of doing this but it potentially runs the validations twice, which in my case would be bad.
EDIT:
my custom validator looks like this
def sa_id_validator
#note this is specific to South African id's
id_makeup = /(\d{6})(\d{4})(\d{1})(\d{1})(\d{1})/.match(#id_number)
birthdate = /(\d{2})(\d{2})(\d{2})/.match(id_makeup[1])
citizenship = id_makeup[3]
variable = id_makeup[4]
validator_key = id_makeup[5]
birthdate_validator(birthdate) && citizenship_validator(citizenship) && variable_validator(variable) && id_algorithm(id_makeup[0], validator_key)
end
private
def birthdate_validator(birthdate)
Date.valid_date?(birthdate[1].to_i,birthdate[2].to_i,birthdate[3].to_i)
end
def citizenship_validator(citizenship)
/[0]|[1]/.match(citizenship)
end
def variable_validator(variable)
/[8]|[9]/.match(variable)
end
def id_algorithm(id_num, validator_key)
odd_numbers = digits_at_odd_positions
even_numbers = digits_at_even_positions
# step1: the sum off all the digits in odd positions excluding the last digit.
odd_numbers.pop
a = odd_numbers.inject {|sum, x| sum + x}
# step2: concate all the digits in the even positions.
b = even_numbers.join.to_i
# step3: multiply step2 by 2 then add all the numbers in the result together
b_multiplied = (b*2)
b_multiplied_array = b_multiplied.to_s.split('')
int_array = b_multiplied_array.collect{|i| i.to_i}
c = int_array.inject {|sum, x| sum + x}
# step4: add the result from step 1 and 3 together
d = a + c
# step5: the last digit of the id must equal the result of step 4 mod 10, subtracted from 10
return false unless
validator_key == 10 - (d % 10)
end
def digits_at_odd_positions
id_num_as_array.values_at(*id_num_as_array.each_index.select(&:even?))
end
def digits_at_even_positions
id_num_as_array.values_at(*id_num_as_array.each_index.select(&:odd?))
end
def id_num_as_array
id_number.split('').map(&:to_i)
end
end
if i add the :calculations_ok => true attribute to my validation, and then pass in a 12 digit number instead i get this error:
i.valid?
NoMethodError: undefined method `[]' for nil:NilClass
from /home/ruberto/work/toolkit_3/toolkit/lib/id_validator.rb:17:in `sa_id_validator'
so you can see its getting to the custom validation even though it should have failed the validates_length_of :id_number??
I am not quite sure but i have read at some blog that Rails always runs all validations even if the first one is invalid.
What you can do is to make your custom method in such a way that it would become flexible or bouncy in such a way that i would handle all the cases.
This answer would definitely help you.
Hope it would answer your question
This is the stripped down version of my model .
model Paper
PAPER_STARTING_NUMBER = 1
validate_uniqueness_of :number, :allow_blank => true
before_create :alocate_paper_number
def alocate_paper_number
return true if self.number.present?
p_number = Paper.maximum('number') || Paper::PAPER_STARTING_NUMBER
self.number = p_number >= Paper::PAPER_STARTING_NUMBER ? p_number+1 : Paper::PAPER_STARTING_NUMBER
return true
end
end
the problem is I have duplicates in the number column .
Any ideas why and how I can fix this without changing the callback .
I know I could add a uniqueness validation on the database or make a sequence on that column , any other ideas ?
First you have to understand the order of callbacks :
(-) save
(-) valid
(1) before_validation
(-) validate
(2) after_validation
(3) before_save
(4) before_create
(-) create
(5) after_create
(6) after_save
(7) after_commit
So as you can see , it validates the uniquity of your number attribute, and then before_create can at its own disposal go against what your validation wants to accomplish.
In regards to a more cleaner architecture, I would put both of these ideas together in your custom model, as it doesn't seem that the number can be choosen by the User. It's just an incrementer, right?
def alocate_paper_number
p_number = Paper.maximum('number') || Paper::PAPER_STARTING_NUMBER
self.number = p_number + 1
end
That snippet alone, would prevent duplicates, as it always increments upwards ( unless, there's the possibility of the number going the other way that I'm not aware of ), and also there's no reason to return all those trues. Its true enough!
It is in de docs. validate_uniqueness_of TRIES to make it unique. But if two processes add one record at the same time, they both can contain the same number.
If you want to guarantee uniqueness, let the database do it. But because that is different for each DB, Rails does not support it by design.
It's explained here: http://guides.rubyonrails.org/active_record_validations_callbacks.html#uniqueness
With the solution: "To avoid that, you must create a unique index in your database."
How I fixed it ( bare in mind that I couldn't return a validation error )
I've added a uniquness index on the number column ( as mu and Hugo suggested )
and because I couldn't return a validation error in the controller
class PaperController < ApplicationController
def create
begin
#paper.save
rescue ActiveRecord::RecordNotUnique
#paper.number = nil
create
end
end
end
I have an app where there is always a current contest (defined by start_date and end_date datetime). I have the following code in the application_controller.rb as a before_filter.
def load_contest
#contest_last = Contest.last
#contest_last.present? ? #contest_leftover = (#contest_last.end_date.utc - Time.now.utc).to_i : #contest_leftover = 0
if #contest_last.nil?
Contest.create(:start_date => Time.now.utc, :end_date => Time.now.utc + 10.minutes)
elsif #contest_leftover < 0
#winner = Organization.order('votes_count DESC').first
#contest_last.update_attributes!(:organization_id => #winner.id, :winner_votes => #winner.votes_count) if #winner.present?
Organization.update_all(:votes_count => 0)
Contest.create(:start_date => #contest_last.end_date.utc, :end_date => Time.now.utc + 10.minutes)
end
end
My questions:
1) I would like to change the :end_date to something that signifies next Sunday at a certain time (eg. next Sunday at 8pm). Similarly, I could then set the :start_date to to the previous Sunday at a certain time. I saw that there is a sunday() class (http://api.rubyonrails.org/classes/Time.html#method-i-sunday), but not sure how to specify a certain time on that day.
2) For this situation of always wanting the current contest, is there a better way of loading it in the app? Would caching it be better and then reloading if a new one is created? Not sure how this would be done, but seems to be more efficient.
Thanks!
Start with this snippet. In rails, given a date/time object d, the expression
d - wday.day
Gives the last Sunday not after that date. i.e. if the day itself is a Sunday the result will be d.
Then to get the next sunday:
sunday1 = d - wday.day
sunday2 = sunday1 + 7.day
And to set the times for both:
sunday1 = (d - wday.day).change(:hour => 8,:min => 30)
sunday2 = (sunday1 + 7.day).change(:hour => 20)
No need for any external modules. Rails (but not core ruby!) has all the date/time manipulation functionality you need.
Note how the change function works: if you pass it :hour only, it will reset the minute and second to 0. If you pass it :hour and :minute, it will reset only the second. See the docs for details.
One last thing: be mindful of what time zone you are using.