Gabriel
Gabriel

Reputation: 641

Rails date of birth confirmation validation

I want to verify my user for their registration. I already have their date of birth stored in my database since they were sent over from an API client. I want the user to confirm their date of birth so I set up a Rails confirmation validation. I have in my params date_of_birth_confirmation and that is what I'm using in my form.

          <%= f.label :date_of_birth_confirmation, "DATE OF BIRTH (MM/DD/YYYY)" %>
          <%= f.date_select :date_of_birth_confirmation, ...

I am able to submit and get my params but the confirmation validation isn't working. My params come through as

  "user" => { "date_of_birth_confirmation(2i)"=>"10", "date_of_birth_confirmation(3i)"=>"5", "date_of_birth_confirmation(1i)"=>"1927" }

which I believe is expected with the date select. From what I understand if it was just the user's date_of_birth and not their dob_confirmation that Rails would convert that to a date object when it was saved in the database.

But when the comparison is made with the actual date_of_birth value it always fails even if they match. I assumed it was because of how the confirmation validation works. Since date_of_birth_confirmation is just a string that meant I needed to convert the dob_confirmation into a date so they could be compared so this ended up being my solution

  def verification_params
    params[:user][:date_of_birth_confirmation] = format_date_of_birth_confirmation
    params.require(:user).permit(
      :date_of_birth_confirmation
    )
  end

  # Format date_of_birth_confirmation to be a date so the comparison can be made with date_of_birth
  def format_date_of_birth_confirmation
    year = params[:user].delete("date_of_birth_confirmation(1i)")
    month = params[:user].delete("date_of_birth_confirmation(2i)")
    day = params[:user].delete("date_of_birth_confirmation(3i)")

    return nil unless year.present? && month.present? && day.present?

    "#{year}-#{month}-#{day}".to_date
  end

This appears to work when I test it. Which is good!

But I wanted to know if there a better solution out there? I'm not entirely sure if there is a more "railsy" way to go about this.

Upvotes: 0

Views: 833

Answers (3)

Gabriel
Gabriel

Reputation: 641

I ended up changing the controller logic to instead put the logic of formatting the date_of_birth_confirmation in the model which is probably where it better fits.

   # User.rb
   before_validation :format_date_of_birth_confirmation

   def format_date_of_birth_confirmation
    return if date_of_birth_confirmation.blank?
    return if date_of_birth_confirmation.is_a?(Date)

    year = date_of_birth_confirmation[1]
    month = date_of_birth_confirmation[2]
    day = date_of_birth_confirmation[3]

    return unless year && month && day

    assign_attributes(date_of_birth_confirmation: "#{year}-#{month}-#{day}".to_date)
  end

Upvotes: 0

rmlockerd
rmlockerd

Reputation: 4136

The short answer is no, there's not a more direct 'rails-y' way than what you are doing since the date_select always just returns the raw selected values as independent strings.

Upvotes: 1

Schwern
Schwern

Reputation: 165606

Use validations.

First, validate that date_of_birth is a Date, not blank, and a reasonable one. I use validates_timeliness.

class User < ApplicationRecord
  # Validate the actual date of birth.
  validates :date_of_birth,
    confirmation: true,
    presence: true,
    timeliness: {
      type => :date,
      # They're not from the future.
      on_or_before: lambda { Date.current },
      # They're not 120.
      after: '1900-01-01'
    }
end

Then make a Form model to validate and process the form input. By including ActiveModel::Model we get a class that can do many of the same things as a Record, but there's no table.

class DateForm
  include ActiveModel::Model

  attr_accessor :year, :month, :day

  validates :year, :month, :day
    # They must be positive integers
    presence: true,
    numericality: { only_integer: true, greater_than_or_equal_to: 1 }

  def to_date
    Date.new(year, month, day)
  end
end

Then you can use DateForm in your controller to validate the form and pass the date along to the User.

def new
  @birth_date = DateForm.new
  @birth_date_confirmation = DateForm.new
  @user = User.new
end

def create
  @user = User.new(params[:user])
  @birth_date = DateForm.new(
    year = params[:birth_date]["date_of_birth(1i)"]
    month = params[:birth_date]["date_of_birth(2i)"]
    day = params[:birth_date]["date_of_birth(3i)"]
  )
  @birth_date_confirmation = DateForm.new(
    year = params[:birth_date_confirmation]["date_of_birth_confirmation(1i)"]
    month = params[:birth_date_confirmation]["date_of_birth_confirmation(2i)"]
    day = params[:birth_date_confirmation]["date_of_birth_confirmation(3i)"]
  )
  render 'new' if !@birth_date.valid? || !@birth_date_confirmation.valid?

  @user.date_of_birth = @birth_date.to_date
  @user.date_of_birth_confirmation = @birth_date_confirmation.to_date

  if @user.save
    redirect_to @user
  else
    render 'new'
  end
end

And use @birth_date and @birth_date_confirmation in your view as their own forms.

form_for(@birth_date, as: :birth_date do |f|
  ...
end

form_for(@birth_date_confirmation, as: :birth_date_confirmation) do |f|
  ...
end

This also gives you a model you can easily unit test making your controller tests simpler.

Upvotes: 0

Related Questions