custom validation gives blank field after validation which is not right - ruby-on-rails

I have a custom validation in my model like this:
class Appointment < ActiveRecord::Base
#VIRTUAL ATTRIBUTES
attr_accessor :start_date, :start_time, :duration
#RELATIONSHIPS
belongs_to :task
#VALIDATIONS
before_validation :convert_to_datetime
before_validation :dur
validates :duration, presence: true
validate :is_date_nil
validate :time_collision_validation, if: :is_appointments_not_empty
validate :check_time
after_save :save_start_date
def is_appointments_not_empty
Appointment.all.present?
end
def check_time
start_at = Time.parse("#{#start_date} #{#start_time}")
if start_at < Time.now
errors.add(:start_date, "Cannot input past times")
end
end
def convert_to_datetime
unless #start_date.blank? && #start_time.blank?
self.start_at = Time.parse("#{#start_date} #{#start_time}")
end
end
def dur
if #start_date.present? && #start_time.present? && #duration.present?
self.end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
end
end
def time_collision_validation
appointments = Appointment.all
if #start_date.present? && #start_time.present? && duration == 0.to_s
start_at = Time.parse("#{#start_date} #{#start_time}")
end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
appointments.each do |a|
if start_at <= a.end_at - (2*60*60) && start_at >= a.start_at - (1*60*60)
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{a.start_at.strftime("%I:%M%p")} of #{a.start_at.strftime("%d/%m/%Y")}
to #{a.end_at.strftime("%I:%M%p")} of #{a.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
elsif #start_date.present? && #start_time.present? && duration.present?
start_at = Time.parse("#{#start_date} #{#start_time}")
end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
appointments.each do |a|
if start_at <= a.end_at - (2*60*60) && a.start_at <= end_at
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{a.start_at.strftime("%I:%M%p")} of #{a.start_at.strftime("%d/%m/%Y")}
to #{a.end_at.strftime("%I:%M%p")} of #{a.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
end
end
def is_date_nil
if #start_date.blank? && #start_time.blank?
errors.add(:start_date, "Start date can't be blank")
errors.add(:start_time, "Start time can't be blank")
end
if #start_date.blank? && #start_time.present?
errors.add(:start_date, "Start date can't be blank")
end
if #start_time.blank? && #start_date.present?
errors.add(:start_time, "Start time can't be blank")
end
end
def start_date=(date)
#start_date = Date.strptime(date, "%d/%m/%Y") if date.present?
end
# def save_start_date
# #start_date = Date.strptime(#start_date, "%d/%m/%Y") if #start_date.present?
# end
# def save_start_date
# #start_date = Date.parse(#start_date).strftime("%d/%m/%Y")if #start_date.present?
# end
def start_time=(time)
#start_time = Time.parse(time).strftime("%H:%M:%S") if time.present?
end
def duration=(duration)
#duration = duration if duration.present?
end
# def start_date
# #start_date.strftime("%d/%m/%Y") if start_at.present? # || start_at.strftime("%d/%m/%Y") if start_at.present?
# end
def start_date
unless #start_date.blank?
#start_date.strftime("%d/%m/%Y")
end
# start_at.strftime("%d/%m/%Y") if start_at.present?
end
def start_time
#start_time || start_at.strftime("%I:%M%p") if start_at.present?
end
# def duration
# #duration || 9
# end
end
After this time_collision_validation executes, the value fields are blank which I don't want because I'm concerned with UX. ie: start_date and start_time fields are blank.
When I checked the value attribute in input in the HTML source code, the value contains a date string. I wonder why it does not show in the field.
Can somebody help me with this and explain what is going on please? >.<

validate :time_collision_validation
def time_collision_validation
appointments = Appointment.all
if self.start_date.present? && self.start_time.present? && duration.present?
start_at = Time.parse("#{self.start_date} #{self.start_time}")
end_at = Time.parse("#{self.start_date} #{self.start_time}") + (self.duration.to_f.hours)
appointments.each do |appointment|
if duration == 0.to_s
duration_ok = start_at >= appointment.start_at - (1.hours)
else
duration_ok = appointment.start_at <= end_at
end
if start_at <= appointment.end_at - (2.hours) && duration_ok
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{appointment.start_at.strftime("%I:%M%p")} of #{appointment.start_at.strftime("%d/%m/%Y")}
to #{appointment.end_at.strftime("%I:%M%p")} of #{appointment.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
end
end
Notes:
you variously refer to duration and self.duration. For readability i would always use self.duration rather than duration or self.duration as it makes it clear to the reader that you are talking about a method/field of the current object rather than a local variable. I've change all instances of referencing methods/fields of the current object to self.methodname
you had a lot of repetition shared between the two if cases. I've refactored these to avoid repetition.
2*60*60 is like saying 2.hours and the latter is much more readable.
what are start_date and start_time - are they strings or date/time objects?
rather than loading every appointment in your entire database, and cycling through them, it would be much more efficient to just search for a single colliding other appointment. if you find one then you can add the errors. I was tempted to do this here but it's not clear exactly what's going on with your database.

Related

Rails speed up database with code refactor for process remover

I've got worker class which removes InquiryProcess older than x time (default should be set to 6 months). Potentially it will be a large data scale so is there any chance to speed up deletions with code below?
class OldProcessRemover
def initialize(date: 6.months.ago)
#date = date
end
attr_reader :date
def call
remove_loan
remove_checking_account
end
private
def remove_loan
loan_template = InquiryTemplate.find_by(inquiry_process_name: InquiryTemplate::LOAN_APPLICATION_PROCESS_NAME)
loan_template.inquiry_processes.where('created_at <= ?', date).each(&:destroy)
end
def remove_checking_account
checking_account_template = InquiryTemplate.find_by(
inquiry_process_name: InquiryTemplate::CHECKING_ACCOUNT_OPENING_PROCESS_NAME,
)
checking_account_template.inquiry_processes.where('created_at <= ?', date).each(&:destroy)
end
end
Maybe somewhere I could use find_in_batches ?. I don't think these methods are single responsibility, so refactor will helped either.
class OldProcessRemover
def initialize(date: 6.months.ago)
#date = date
end
attr_reader :date
def call
remove_loan
remove_checking_account
end
private
def remove_loan
remove_processes!(InquiryTemplate::LOAN_APPLICATION_PROCESS_NAME)
end
def remove_checking_account
remove_processes!(InquiryTemplate::CHECKING_ACCOUNT_OPENING_PROCESS_NAME)
end
def remove_processes!(process_name)
account_template = InquiryTemplate.find_by(
inquiry_process_name: process_name
)
account_template.inquiry_processes
.where('created_at <= ?', date)
.find_in_batches { |group| group.destroy_all }
end
end
I don't think there is any major difference between using .find_in_batches { |group| group.destroy_all } or .find_each {|record| record.destroy } here.

Greater/Lower than works but equals doesn't on >= and <= on .where()

I have a Rails 4 app and I'm trying to make a simple search for my invoices with 3 optional arguments: Name of the client, Start Date, End Date.
The search works fine mostly, if I put a start date and an end date it works for < and >, but eventhough i used >= and <=, if the invoice date is the same to either start or end, it just won't show on the result list.
The tables used look like this:
Client Table
ID
Name
The rest of the fields aren't necessary
Invoice Table
ID
Client_ID
Total_Price
Created_At *only here for relevance*
My Invoice Controller Search method looks like this:
def search
if request.post?
#count = 0
#invoices = Invoice.all
if params[:start_date].present?
#invoices = Invoice.invoices_by_date(#invoices, params[:start_date], 'start')
if #invoices.present?
#count = 1
else
#count = 2
end
end
if params[:end_date].present?
#invoices = Invoice.invoices_by_date(#invoices, params[:end_date], 'end')
if #invoices.present?
#count = 1
else
#count = 2
end
end
if params[:name].present?
#invoices = Invoice.invoices_by_client(#invoices, params[:name])
if #invoices.present?
#count = 1
else
#count = 2
end
end
if #count == 2
flash.now[:danger] = "No results found."
#invoices = nil
end
#name = params[:name]
#start_date = params[:start_date]
#end_date = params[:end_date]
end
end
And the Invoice Model methods i use look like this:
def self.invoices_by_client(invoices, name)
invoices= invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("clients.name LIKE ?", "%#{name}%")
.references(:client)
return invoices
end
def self.invoices_by_date(invoices, date, modifier)
if modifier == 'start'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at >= ?", date)
.references(:client)
elsif modifier == 'end'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at <= ? ", date)
.references(:client)
end
return invoices
end
It probably isn't the best solution overall and I don't know if i did anything wrong so it would be great if you guys could help me with this.
I followed Alejandro's advice and messed around with the time aswell as the date, something like this:
if modifier == 'start'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at >= ?", "#{date} 00:00:00") // Added the start time
.references(:client)
elsif modifier == 'end'
invoices = invoices.includes(:client)
.select('invoices.created_at', 'invoices.total_price', 'clients.name')
.where("invoices.created_at <= ? ", "#{date} 23:59:59") // Added end time aswell
.references(:client)
end
I forced the time for the start date as 00:00:00 and the time for the end date as 23:59:59 and it worked as desired. Thank you for the help man and i hope this helps other people!

Correct way to add helper functions for an rspec spec

So I need a helper function to created 'unprocessed tweets' similar to how I might receive them from the Twitter API gem, so I can test my models functionality under certain conditions.
To do this I added a helper function inside my objects describe, like so:
describe Tag, :type => :model do
# Helpers
###
def unprocessed_tweets(count, name, start_date, end_date)
tweets = []
count.times do |index|
tweet = OpenStruct.new
tweet.favorite_count = "3"
tweet.filter_level = "high"
tweet.retweet_count = "12"
tweet.text = "#{name}"
if index == 0
tweet.created_at = start_date
elsif index == (count-1)
tweet.created_at = end_date
else
tweet.created_at = start_date
end
tweets.push tweet
end
tweets
end
I also added a test to make sure my helpers doing as I expect in the long term:
it "has a helper for generated unprocessed tweets" do
tag_name = "justin"
start_date = '2015-09-12 2:31:32 0'
end_date = '2015-09-13 2:31:32 0'
tweets = unprocessed_tweets(3, tag_name, start_date, end_date)
expect(tweets.size).to eq 3
expect(tweets.first.favorite_count).to eq "3"
expect(tweets.first.created_at).to eq start_date
expect(tweets.last.created_at).to eq end_date
expect(tweets.last.text).to eq tag_name
end
Is this best practice for this?
You can create a new file in spec/support called tweet_helpers.rb and put this content in it:
module TweetHelpers
def unprocessed_tweets(count, name, start_date, end_date)
tweets = []
count.times do |index|
tweet = OpenStruct.new
tweet.favorite_count = "3"
tweet.filter_level = "high"
tweet.retweet_count = "12"
tweet.text = "#{name}"
if index == 0
tweet.created_at = start_date
elsif index == (count-1)
tweet.created_at = end_date
else
tweet.created_at = start_date
end
tweets.push tweet
end
tweets
end
end
And your spec test file should look like this:
require './spec/support/tweet_helpers'
RSpec.configure do |c|
c.include TweetHelpers
end
RSpec.describe "an example group" do
it "has a helper for generated unprocessed tweets" do
tag_name = "justin"
start_date = '2015-09-12 2:31:32 0'
end_date = '2015-09-13 2:31:32 0'
tweets = unprocessed_tweets(3, tag_name, start_date, end_date)
expect(tweets.size).to eq 3
expect(tweets.first.favorite_count).to eq "3"
expect(tweets.first.created_at).to eq start_date
expect(tweets.last.created_at).to eq end_date
expect(tweets.last.text).to eq tag_name
end
end
I think this is good practice to define your helper methods in a separate module rather than crowding the spec test file itself.
See this for more information and examples.

Saving to DB from Model.rb

When an new order_preview is created, I call USPS for shipping options. If a user updates their zip, I would like the ship_option to reset
Edit: I am no longer calling the intial API call from the view, rather I do an after_create method in the controller.
def get_ship_options
ship_options = {}
#order_preview.fedex_rates.each do |k, v|
if k.service_name == "FedEx Ground Home Delivery" || k.service_name == "FedEx 2 Day" || k.service_name == "FedEx Standard Overnight"
ship_options["#{k.service_name}"] = "#{number_to_currency(k.price.to_f / 100)}"
end
end
#order_preview.usps_rates.each do |k, v|
if k.service_name == "USPS Priority Mail 1-Day"
ship_options["#{k.service_name}"] = "#{number_to_currency(k.price.to_f / 100)}"
end
end
#order_preview.ship_option_hash = ship_options.map { |k,v| ["#{k} - #{v}","#{k} - #{v}" ] }
#order_preview.save
end
I tried using the answers you guys provided, but the before_save didn't actually save the shiphash the way #order_preview.save does at the end of the above method.
I tried using the same idea, but zip_changed? doesn't work in the controller.
How can I save the new hash that is pulled from the model directly over to the #order_preview ?
From the model I now have
Model.rb
def clear_hash
if zip_changed?
get_shipping_rates
end
end
and
ship_options = {}
fedex_rates.each do |k, v|
if k.service_name == "FedEx Ground Home Delivery" || k.service_name == "FedEx 2 Day" || k.service_name == "FedEx Standard Overnight"
ship_options["#{k.service_name}"] = "#{number_to_currency(k.price.to_f / 100)}"
end
end
usps_rates.each do |k, v|
if k.service_name == "USPS Priority Mail 1-Day"
ship_options["#{k.service_name}"] = "#{number_to_currency(k.price.to_f / 100)}"
end
end
ship_option_hash = ship_options.map { |k,v| ["#{k} - #{v}","#{k} - #{v}" ] }
**save ship_option_hash to #order_preview.ship_option_hash**
class OrderPreview
before_save :check_state
def check_state
if zip_changed?
ship_option_hash = nil
end
end
...
end
class OrderPreviewController
def update
#order_preview.update(order_preview_params)
end
...
end
Try changing your callback from after_save to before_save. Record considered changed until the changes are not persisted. Changes are lost when you save your object, that's why your record is unchanged when you check for changes in after_save callback.
It should work this way:
before_save :clear_hash, if: :zip_changed?
def clear_hash
ship_option_hash = nil
end
This way the changes will be saved, because you use before_save. In your code, changes were not saved, because you used after_save callback
You controller:
def update
respond_to do |format|
if #order_preview.update(order_preview_params)
flash[:notice] = "Record was successfully updated"
else
flash[:alert] = "Record was not updated"
end
end
end

Calculate days until next birthday in Rails

I have a model with a date column called birthday.
How would I calculate the number of days until the user's next birthday?
Here's a simple way. You'll want to make sure to catch the case where it's already passed this year (and also the one where it hasn't passed yet)
class User < ActiveRecord::Base
attr_accessible :birthday
def days_until_birthday
bday = Date.new(Date.today.year, birthday.month, birthday.day)
bday += 1.year if Date.today >= bday
(bday - Date.today).to_i
end
end
And to prove it! (all I've added is the timecop gem to keep the calculations accurate as of today (2012-10-16)
require 'test_helper'
class UserTest < ActiveSupport::TestCase
setup do
Timecop.travel("2012-10-16".to_date)
end
teardown do
Timecop.return
end
test "already passed" do
user = User.new birthday: "1978-08-24"
assert_equal 313, user.days_until_birthday
end
test "coming soon" do
user = User.new birthday: "1978-10-31"
assert_equal 16, user.days_until_birthday
end
end
Try this
require 'date'
def days_to_next_bday(bday)
d = Date.parse(bday)
next_year = Date.today.year + 1
next_bday = "#{d.day}-#{d.month}-#{next_year}"
(Date.parse(next_bday) - Date.today).to_i
end
puts days_to_next_bday("26-3-1985")
Having a swipe at this:
require 'date'
bday = Date.new(1973,10,8) // substitute your records date here.
this_year = Date.new(Date.today.year, bday.month, bday.day )
if this_year > Date.today
puts this_year - Date.today
else
puts Date.new(Date.today.year + 1, bday.month, bday.day ) - Date.today
end
I'm not sure if Rails gives you anything that makes that much easier.
Here's another way to approach this with lesser-known methods, but they make the code more self-explanatory.
Also, this works with birth dates on a February 29th.
class User < ActiveRecord::Base
attr_accessible :birthday
def next_birthday
options = { year: Date.today.year }
if birthday.month == 2 && birthday.day == 29 && !Date.leap?(Date.today.year)
options[:day] = 28
end
birthday.change(options).tap do |next_birthday|
next_birthday.advance(years: 1) if next_birthday.past?
end
end
end
And of course, the number of days until the next birthday is:
(user.next_birthday - Date.today).to_i

Resources