Rails: Custom validations using ActiveModel

918 views Asked by At

I am currently trying to make custom validations work with an input of dates, but, unfortunately, it doesn't seem to work.

There are two pages inside the application, Index page and Search page. Inside the index page there is a text field that takes in a date. I am using Chronic gem which parses text into dates. If the date is invalid, Chronic returns nil. If it is valid, it redirects to search page and shows the date.

The code I wrote so far doesn't seem to work properly, but what I want to achieve is..

1) to validate that Chronic doesn't return nil

2) to validate that date is greater than today's date

Please note that I am not using a database with this, I just want to be able to validate inputted date without ActiveRecord. If someone could help me with this, your help will be greatly appreciated.

views/main/index.html.erb

<%= form_tag({controller: "main", action: "search"}, method: "get") do %>
    <%= label_tag(:q, "Enter Date:") %>
    <%= text_field_tag(:q) %>
    <%= submit_tag "Explore", name: nil %>
<% end %>

views/main/search.html.erb

<%= @show_date %>

main_controller.rb

def search

    passed_info = Main.new

    if passed_info.valid_date?
        @show_date = passed_info
    else
        flash[:error] = "Please enter correct date!"
        render :index => 'new'
    end

end

models/main.rb

class Main

  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend  ActiveModel::Naming

  attr_accessor :q

  validates_presence_of :q

  def initialize(params={})
    params.each do |attr, value|
    self.public_send("#{attr}=", value)
    end if params
  end

  def persisted?
    false
  end

  def valid_date?
    require 'chronic'
    if Chronic.parse(q).nil? || Chronic.parse(q) < Time.today
        errors.add(:q, "is missing or invalid")
    end
  end

end

EDIT:

this is what goes wrong...

localhost:3000

enter image description here

then it redirects to ..

localhost:3000/main/search?utf8=%E2%9C%93&q=invalid+date+test

enter image description here

No validation, no date, nothing..

2

There are 2 answers

0
Tim On BEST ANSWER

Just as correctly suggested by ABMagil, I would like to post the full solution to my answer. In fact, this answer can apply really to anyone who wants to use validations using ActiveModel, with or without Chronic gem or dates involved. It can act as a valid template so to speak.

Frankly, most of my mistakes came from a really poor, at the time, understanding of what I actually tried to achieve. Most of the code needed major refactoring, see below the updates that I had to make. I tried to keep the code as well documented as possible.

Solution:

views/main/index.html.erb

<%= form_for @search, url: { action: "search" }, 
                      html: { method: :get } do |f| %>

  # Displays error messages if there are any.
  <% if @search.errors.any? %>
    The form contains <%= pluralize(@search.errors.count, "error") %>.<br />
    <% @search.errors.each do |attr, msg| %>
      <%= msg %><br />
    <% end %>
  <% end %>

  <%= f.label :q, "Enter Date:" %>
  <%= f.text_field :q %>
  <%= f.submit "Explore", :class => 'submit' %>
<% end %>

views/main/search.html.erb - same as before

<%= @show_date %>

main_controller.rb

def index
  # Initializes a string from the form to a variable.
  @search = Search.new
end

def search
  # Retrieves the input from the form.
  @search = Search.new(params[:search])

  # Checks for validity, 
  # If valid, converts a valid string into a date.
  # Redirects to search.html.erb 
  # If not valid, renders a new index.html.erb template.
  if @search.valid?
    @show_date = (Chronic.parse(params[:search][:q])).to_date   
  else
    render :action => 'index'   
  end
end

models/main.rb

class Main
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend  ActiveModel::Naming

  # Accepts the passed attribute from the form.
  attr_accessor :q

  # If form is submitted blank, then output the prompting message.
  validates_presence_of :q, :message => "This text field can't be blank!"
  # Two custom validations that check if passed string converts to a valid date.
  validate :date_is_valid
  validate :date_not_before_today

  # Initializes the attributes from the form.
  def initialize(attributes = {})
    attributes.each do |name, value|
      send("#{name}=", value)
    end
  end

  # Checks for persistence, i.e. if it's a new record and it wasn't destroyed. 
  # Otherwise returns false.
  def persisted?
    false
  end

  # ABMagil's code used for custom date validations
  private
    require 'chronic'
    def date_is_valid
      if Chronic.parse(q).nil?
        errors.add(:base, "Date is invalid")
      end
    end
    def date_not_before_today
      if !Chronic.parse(q).nil?
        if Chronic.parse(q) < Date.today
          errors.add(:base, "Date cannot be before today")
        end
      end
    end
end

Result:

Result

0
ABMagil On

The Problem

Be more careful with return values. When you try to guard your controller with if valid_date?, what you're doing is checking to see if valid_date? returns false. If the parse fails, the return value is the output of errors.add, which in turn is the output of Array#<<. Relevantly, the output isn't nil or false, so it evaluates to true, thus the if clause passes and you move forward.

Potential Solution

You probably want to let the Rails Validation Framework do more work for you. Instead of treating valid_date? as a public method which the controller calls, call the valid? method that gets added by ActiveModel::Validations. valid? will return a boolean, based on whether all the model validations pass. Thus, you would, as is the Rails Way, call if model_instance.valid? in your controller.

This lets you just write validator methods in your model which express the logic you're trying to write. Right now, you have all validation logic for dates in a single method, with a single error message. Instead, you could put two methods, which add more descriptive individual error methods.

class YourClass
  include ActiveModel::Validations

  validate :date_is_valid
  validate :date_not_before_today

  private
  def date_is_valid
    if Chronic.parse(q).nil?
      errors.add(:q, "Date is invalid")
    end
  end
  def date_not_before_today
    if Chronic.parse(q) < Date.today
      errors.add(:q, "Date cannot be before today")
    end
  end
end