Validating datetime entries in a model - ruby-on-rails

I'm playing with database and model in Rails, and I have a 'create tours' migration that looks like this
class CreateTours < ActiveRecord::Migration[5.2]
def change
create_table :tours do |t|
t.string :name
t.integer :price
t.datetime :starts_at
t.datetime :ends_at
end
end
end
I added a model Tour
class Tour < ApplicationRecord
has_many :bookings, dependent: :destroy
validates :name, presence: true,
uniqueness: { case_sensitive: false }
validates :starts_at, presence: true
validates :ends_at, presence: true
validates :price, presence: true,
numericality: { greater_than: 0 }
validate :end_time_is_valid
def end_time_is_valid
error_msg = 'Trip cannot ends before it began'
errors.add(:ends_at, error_msg) if ends_at < starts_at
end
end
And I added RSpec
RSpec.describe Tour do
subject do
described_class
.create(
name: 'Tour 1',
starts_at: Time.zone.now,
ends_at: Time.zone.now + 7200,
price: 200
)
end
let(:invalid_flight) do
described_class.create(
name: 'Tour 2',
starts_at: Time.zone.now,
ends_at: Time.zone.now - 7200,
price: 200
)
end
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:starts_at) }
it { is_expected.to validate_presence_of(:ends_at) }
it { is_expected.to validate_presence_of(:price) }
it { is_expected.to have_many(:bookings) }
end
Now, I'm getting an errors like
Flight should validate that :starts_at cannot be empty/falsy
Failure/Error: errors.add(:ends_at, error_msg) if ends_at < starts_at
ArgumentError:
comparison of ActiveSupport::TimeWithZone with nil failed
# ./app/models/tour.rb:18:in `<'
# ./app/models/tour.rb:18:in `end_time_is_valid'
# ./spec/models/tour_spec.rb:22:in `block (2 levels) in <main>'
What I'm trying to add is a validation that the end tour time cannot be before start tour time.
But the custom validation fails, and I'm not sure why.
I'm playing with Rails and I'm not sure how should I do this.
I'm using shoulda-matchers for testing.
Some simple models like User, which just has simple name and email validation is passing (no custom validations).
How do I validate this? Should I use numericality here as well?

Your validation is still invoked when starts_at is blank/nil, even though you've defined a presence validation: there's no hierarchy to the validations, so one failing can't cause others to be skipped.
Consequently, your validation method should just skip the check if either value is blank -- leaving the presence validation to record the error.
def end_time_is_valid
return if starts_at.blank? || ends_at.blank?
error_msg = 'Trip cannot ends before it began'
errors.add(:ends_at, error_msg) if ends_at < starts_at
end

Related

Rails Model Custom Validation Won't Work On Update

I try to create some model validation on my Coupon Model
evrything work on create, but when i try to update data the validation won't validate
the first validation is to raise error if percentage more than 100
the second validation is to validate if date start bigger than date expired it will throw some error
my model in this code:
# frozen_string_literal: true
class Coupon < ApplicationRecord
has_many :transactions, dependent: :restrict_with_exception
has_one :battery, through: :transactions
enum :discount_type, %i[percentage nominal]
validates :code, format: { with: /\A[0-9A-Z]+\Z/ },
presence: true,
uniqueness: true,
length: { maximum: 50 },
on: :create,
allow_nil: false
validate :discount_value, :discount_type
validate :discount_value_percentage
validates :start_at, :expired_at, presence: true
validate :end_date_after_start_date
def discount_value_percentage
return unless discount_type == 'percentage' && discount_value > 100
errors.add(:discount_value, 'discount value percentage cannot over 100%')
end
def end_date_after_start_date
return if expired_at.blank? || start_at.blank?
return unless expired_at < start_at
errors.add(:expired_at, 'must be after the start date')
end
end
Please help me to resolve my problem
the validation can validate if date start bigger than date end it will throw error code

Why is numericality validator not working with Active Model Attributes?

I'm using Rails 7 and Ruby 3.1, and Shoulda Matchers for tests, but not Active Record, for I do not need a database.
I want to validate numericality. However, validations do not work. It looks like input is transformed into integer, instead of being validated. I do not understand why that happens.
My model:
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: { in: 1..50 }, numericality: { only_integer: true }
# Some other code...
end
My test:
# spec/models/grid_spec.rb
RSpec.describe Grid, type: :model do
describe 'validations' do
shared_examples 'validates' do |field, range|
it { is_expected.to validate_numericality_of(field).only_integer }
it { is_expected.to validate_inclusion_of(field).in_range(range) }
end
include_examples 'validates', 'rows', 1..50
end
# Some other tests...
end
Nonetheless, my test fails:
Grid validations is expected to validate that :rows looks like an integer
Failure/Error: it { is_expected.to validate_numericality_of(field).only_integer }
Expected Grid to validate that :rows looks like an integer, but this
could not be proved.
After setting :rows to ‹"0.1"› -- which was read back as ‹0› -- the
matcher expected the Grid to be invalid and to produce the validation
error "must be an integer" on :rows. The record was indeed invalid,
but it produced these validation errors instead:
* rows: ["is not included in the list"]
As indicated in the message above, :rows seems to be changing certain
values as they are set, and this could have something to do with why
this test is failing. If you've overridden the writer method for this
attribute, then you may need to change it to make this test pass, or
do something else entirely.
Update
Worse than before, because tests are working but code is not actually working.
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, presence: true, numericality: { only_integer: true, in: 1..50 }
# Some other code...
end
# spec/models/grid_spec.rb
RSpec.describe Grid, type: :model do
describe 'validations' do
shared_examples 'validates' do |field, type, range|
it { is_expected.to validate_presence_of(field) }
it do
validate = validate_numericality_of(field)
.is_greater_than_or_equal_to(range.min)
.is_less_than_or_equal_to(range.max)
.with_message("must be in #{range}")
is_expected.to type == :integer ? validate.only_integer : validate
end
end
include_examples 'validates', 'rows', :integer, 1..50
end
# Some other tests...
end
The underlying problem (or maybe not a problem) is that you're typecasting rows attribute to integer.
>> g = Grid.new(rows: "num"); g.validate
>> g.errors.as_json
=> {:rows=>["is not included in the list"]}
# NOTE: only inclusion errors shows up, making numericality test fail.
To make it more obvious, let's remove inclusion validation:
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, numericality: { only_integer: true }
end
Still, this does not fix numericality test:
# NOTE: still valid
>> Grid.new(rows: "I'm number, trust me").valid?
=> true
# NOTE: because `rows` is typecasted to integer, it will
# return `0` which is numerical.
>> Grid.new(rows: "I'm number, trust me").rows
=> 0
>> Grid.new(rows: 0.1).rows
=> 0
# NOTE: keep in mind, this is the current behavior, which
# might be unexpected.
In the test validate_numericality_of, first of all, expects an invalid record with "0.1", but grid is still valid, which is why it fails.
Besides replacing the underlying validations, like you did, there are a few other options:
You could replace numericality test:
it { expect(Grid.new(rows: "number").valid?).to eq true }
it { expect(Grid.new(rows: "number").rows).to eq 0 }
# give it something not typecastable, like a class.
it { expect(Grid.new(rows: Grid).valid?).to eq false }
Or remove typecast:
attribute :rows
Update
Seems like you're trying to overdo it with validations and typecasting. From what I can see the only issue is just one test, everything else works fine. Anyway, I've came up with a few more workarounds:
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: 1..50, numericality: { only_integer: true }
def rows= arg
# NOTE: you might want to raise an error instead,
# because this validation will go away if you run
# validations again.
errors.add(:rows, "invalid") if (/\d+/ !~ arg.to_s)
super
end
end
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: 1..50, numericality: { only_integer: true }
validate :validate_rows_before_type_cast
def validate_rows_before_type_cast
rows = #attributes.values_before_type_cast["rows"]
errors.add(:rows, :not_a_number) if rows.is_a?(String) && rows !~ /^\d+$/
end
end
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveRecord::AttributeMethods::BeforeTypeCast
attribute :rows, :integer
validates :rows, inclusion: 1..50
# NOTE: this does show "Rows before type cast is not a number"
# maybe you'd want to customize the error message.
validates :rows_before_type_cast, numericality: { only_integer: true }
end
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html
My solution was dividing validations and typecasts into models.
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
end
# app/models/grid_data.rb
class GridData
include ActiveModel::Model
include ActiveModel::Attributes
Grid.attribute_names.each { |name| attribute name }
validates(*attribute_names, presence: true)
validates :rows, numericality: { only_integer: true, in: 1..50 }
end
Specs
# spec/models
RSpec.describe Grid, type: :model do
let(:grid) { described_class.new(**attributes) }
describe 'type cast' do
let(:attributes) { default(rows: '2') }
it 'parses string valid arguments to integer or float' do
expect(grid.rows).to eq 2
end
end
end
RSpec.describe GridData, type: :model do
it 'has same attributes as Grid model' do
expect(described_class.attribute_names).to eq Grid.attribute_names
end
describe 'validations' do
shared_examples 'validates' do |field, type, range|
it { is_expected.to validate_presence_of(field) }
it do
validate = validate_numericality_of(field)
validate = validate.only_integer if type == :integer
expect(subject).to validate
end
it do
expect(subject).to validate_inclusion_of(field)
.in_range(range)
.with_message("must be in #{range}")
end
end
include_examples 'validates', 'rows', :integer, 1..50
end
end
Controller
# app/controller/grids_controller.rb
class GridsController < ApplicationController
def create
#grid_data = GridData.new(**grid_params)
if #grid_data.valid?
play
else
render :new, status: :unprocessable_entity
end
end
private
def grid_params
params.require(:grid_data).permit(*Grid.attribute_names)
end
def play
render :play, status: :created
Grid.new(**#grid_data.attributes).play
end
end

Creating a has_and_belongs_to_many relationship in Rails

I have a User model which is designed after the Michael Hartl RoR tutorial and I am trying to create a new Teacher model. I would like the teacher to have many users but each user to have only one teacher. I created the teacher model with
class CreateTeachers < ActiveRecord::Migration
def change
create_table :teachers do |t|
t.string :name
t.string :email
t.string :phone
t.references :user, index: true, foreign_key: true
t.timestamps null: false
end
end
end
and added has_one :teacher to user.rb. Here is the teachers.rb model
class Teacher < ActiveRecord::Base
has_and_belongs_to_many :users
validates :user_id, presence: true
before_save :downcase_email
validates :name, presence: true,
length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true,
length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
private
# Converts email to all lower-case.
def downcase_email
self.email = email.downcase
end
end
However in my teacher_test.rb test file, things get a little fuzzy. I try this
def setup
#user = users(:michael)
#user2 = users(:archer)
#user3 = users(:lana)
#user4 = users(:mallory)
#teacher = Teacher.new(name: "Phred Willard",
email: "pwillard#test.com",
phone: "1234567890",
user_id: [#user.id,
#user2.id,
#user3.id,
#user4.id])
end
test "should be valid" do
assert #uac.valid?
end
but that fails outright. Did I set my relationship up correctly? I obviously am not adding users correctly since the model fails a validity test. How would I add more users to that teacher? Thanks in advance.
I would like the teacher to have many users but each user to have only one teacher
You only need has_many / belongs_to...
#app/models/user.rb
class User < ActiveRecord::Base
belongs_to :teacher
end
#app/models/teacher.rb
class Teacher < ActiveRecord::Base
has_many :users
end
You'll need to add a teacher_id column in your users table (the opposite of what you have now):
class UpdateUsers < ActiveRecord::Migration
def change
change_table :users do |t|
t.references :teacher, index: true, foreign_key: true #-> teacher_id
end
end
end
--
The error you have is that you're calling user_id on teacher; it should be teacher_id on user:
#teacher = Teacher.new(name: "Phred Willard",
email: "pwillard#test.com",
phone: "1234567890",
user_ids: [#user.id,
#user2.id,
#user3.id,
#user4.id])
This should associate #teacher with the defined #users you've listed.
You'll also want to look at collection_singular_ids for the has_many association, which is why your test is failing.
Your teacher.rb should be
class Teacher < ActiveRecord::Base
has_many :users
before_save :downcase_email
validates :name, presence: true,
length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :email, presence: true,
length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
private
# Converts email to all lower-case.
def downcase_email
self.email = email.downcase
end
end
and user.rb
class User < ActiveRecord::Base
belongs_to :teacher
# rest of your code here ....
end
You need teacher_id column in users table.

Rails model default_value method conflict with presence true

I have this simplified model:
class Contract < ActiveRecord::Base
belongs_to :user belongs_to :plan
before_validation :set_default_is_voided
before_validation :set_default_expiration
validates :user, presence: true
validates :plan, presence: true
validates :contract_date, presence: true
validates :is_voided, presence: true
validates :expiration, presence: true
protected
def set_default_is_voided
if self.is_voided.nil?
self.is_voided = false
ap self.is_voided.present?
ap self.is_voided
end
end
def set_default_expiration
if self.contract_date.present?
self.expiration = self.contract_date+1.month
end
end
end
And this rspec simplified test:
context "Should create default values" do
it "Have to create is_voided" do
user = FactoryGirl.create(:user)
plan = FactoryGirl.create(:planContract)
ap "HERE"
contractDefault = FactoryGirl.create(:contractDefault, plan: plan, user: user)
ap contractDefault
expect(contractDefault.is_voided).to eq(false)
end
it "Have to create expiration" do
#expect(contract.expiration).should eq(Date.today()+1.month)
end
end
FactoryGirl:
FactoryGirl.define do
factory :contractVoid, class:Contract do
end
factory :contractDefault, class:Contract do
contract_date Date.today
end
end
This test fail with an 'is_voided can't be blank'.
And the question is:
Why the method "set_default_is_voided" in before_validation don't pass the presence true validation? Moreover, the self.is_voided.present? return false, why is it happing?
You answered your own question as to why set_default_is_voided doesn't pass the the presence: true validation, namely that self.is_voided.present? returns false, which is how presence: true is determined.
self.is_voided.present? returns false because false.present? == false per A concise explanation of nil v. empty v. blank in Ruby on Rails
See Rails: how do I validate that something is a boolean? for one way to validate that a boolean field is not nil.
See http://www.quora.com/Why-does-Rails-make-false-blank-true for a Q&A on the motivation behind the definition of blank?.

uniqueness test on minitest

I use minitest on Ruby on Rails. Below is my model.
require 'mongoid'
class Person
include Mongoid::Document
index({ pin: 1 }, { unique: true, name: "pin_index" })
field :first_name
field :last_name
field :pin
validates :pin, presence: true, uniqueness: true
validates :first_name, presence: true
validates :last_name, presence: true
end
I try to write model test.I want to write a test that controls whether pin field is unique or not. How can i do this? Any idea?
I try to write a test like below:
it 'must not be valid' do
person_copy = person.dup
person.save
person_copy.save
end
You can write the test like this:
it 'must have unique pin' do
person_copy = person.dup
proc { person_copy.save! }.must_raise(Mongoid::Errors::Validations)
person_copy.errors.must_include(:pin)
end
You can use assert_includes and assert_same to test the error is the right one (about uniqueness):
it 'must not be valid' do
person_copy = person.dup
person.save
person_copy.save
assert_includes person.errors, :pin
assert_same person.errors[:pin], "pin is not unique (replace with actual error message)"
end
Considering you have a fixture already set, you can just do this:
test 'pin must be unique' do
new_person = Person.new(#person.attributes)
refute new_person.valid?
end

Resources