Partially Solving The Date Validation Deficiency Of Rails 3 And Mongoid 2 Models

A while back, I posted a question on stack overflow on how to properly validate a date input from a text field input, without throwing exceptions when the string provided is not a valid date.  The core of the problem is that when a user is allowed to type into a text box, for a date, and they input something invalid, assigning the invalid value to the field on the model will throw an exception, because the field tries to coerce the value into a date immediately.

 

The Original Workaround

The answer I came up with was based on the answer posted to the question, by Rod Paddock, but was a bit of a hack compared to what I wanted (keep in mind that I’m using Mongoid instead of ActiveRecord when looking at this example):

class SomeModel
  include Mongoid::Document

  field :date, :type => Date
  validates_presence_of :date

  def date=(value)
    begin
      date_parsed = value.to_date
      write_attribute :date, date_parsed
      @bad_date = nil
    rescue
      write_attribute :date, nil
      @bad_date = value
    end
  end

  def date
    return @bad_date if @bad_date
    read_attribute :date
  end

end

 

This code effectively accomplishes what I want. It allows me to assign any arbitrary value to the model, validate the input to see if it’s even a valid date format. It let’s me keep the arbitrary value around and return it from the attribute when called. It also prevents bad dates from being considered ok, if you combine it with a ‘validates_presennce_of :date’ validation.

 

Issues With This Workaround

There are a few things that this workaround doesn’t do. For example, it is not maintainable long-term. Every time I have a date in a model, I have to repeat this code. It’s not going to work with calls to .update_attributes or .write_attributes. And, it’s not going to tell you that you have an invalid date in the model’s .errors collection. Instead, it’s going to tell you that the date is blank. No built in validation technique will validate the value before it’s assigned to the field. We could use a custom validation class and have itre-parse the value that comes out of the attribute, though. The downside here is re-parsing the value and throwing / catching another exception, which has a cost associated with it. I’m not sure there’s a way around the parsing / exception catching, but we should at least minimize that to one call.

What I really wanted to do was abstract this solution out into something reusable, that would solve some of the remaining issues.

 

A Better Solution With ActiveSupport::Concern And Meta-Programming

My recent use of ActiveSupport::Concern that I talked about in another post gave me an idea, and I ran with it. I could use a concern as a module to plug into a model, and provide a method that would not only define the date field for me, but provide accessor methods that know how to handle all of the parsing and storage needs that I have. I could also use a better data structure to store the results of the parsing, which would give me a better way to handle a custom validator without having to re-parse the input.

The result of a day’s hacking this weekend, is the following concern:

module Mongoid
  module DateField
    extend ActiveSupport::Concern

    included do
      validates_with Mongoid::DateFieldValidator
    end

    module ClassMethods
      def date_field(name)
        self.class_eval(<<-EOL, __FILE__, __LINE__)
          field :#{name}, :type => Date

          def #{name}=(value)
            set_date_field_value :#{name}, value
          end

          def #{name}
            get_date_field_value :#{name}
          end
        EOL
      end
    end

    def date_fields
      @date_fields ||= {}
    end

    def set_date_field_value(name, value)
      field = get_date_field name
      begin
        parsed_value = value.to_date
        field[:broken_value] = nil
        field[:valid] = true
      rescue
        parsed_value = nil
        field[:broken_value] = value
        field[:valid] = false
      end
      write_attribute name, parsed_value
    end

    def get_date_field_value(name)
      field = get_date_field name
      if !field.key?(:valid) or field[:valid]
        raw_value = read_attribute(name)
        value = raw_value.to_date if raw_value
      else
        value = field[:broken_value]
      end
      value
    end

    def get_date_field(name)
      if date_fields.key? name
        field = date_fields[name]
      else
        field = {}
        date_fields[name] = field
      end
      field
    end

  end

  class DateFieldValidator < ActiveModel::Validator
    def validate(model)
      is_valid = true
      model.date_fields.each do |name, data|
        if !data[:valid]
          is_valid = false
          model.errors.add name, "must be a valid date: MM/DD/YYYY"
        end
      end
      is_valid
    end
  end
end

 

The first thing you’ll notice is that this concern is namespaced for Mongoid. I did this specifically because the solution I built only works with Mongoid, at this point. I don’t use any of the usual ActiveRecord stuff in this project, so there was no need for me to build support for ActiveRecord. Someone else might be able to make it work with ActiveRecord fairly easily, though.

Next, note the nested ClassMethods module. This module name is recognized by ActiveSupport::Concern and tells the concern to turn all of the method inside of it, into class level methods on the class that is including the concern. The end result is that my model will have a ‘date_field’ method that can be called in the class definition.

The implementation of the date_field method uses some meta-programming to inject a few things into the class when the method is called. First, it defines the date field according to the name that you provide. It then defines the accessor methods for reading and writing the attribute’s value. All of this is done inside of a class_eval call, using string injection with <<-EOL … EOL. This causes ruby to execute all of the code in that string in the context of the class on which class_eval is being called. I’m normally not a fan of this style of meta-programming, but I think this is an acceptable use to keep the code clean and easy to read and understand.

The accessor methods don’t do anything more than delegate to another method in the concern. In case of the assignment access, the set_date_field_value method does the parsing and storage of the bad result or good result. The get_date_field_value then does the opposite – checking to see if a bad value is stored and returning either the bad value or the actual attribute value, depending. All of this is facilitated with a simple hash that uses the field name as the key and tells me whether the input value is valid or not.

Last, there is a custom validator class at the bottom of the code. This validator uses the data structure from the concern’s input parsing to determine whether or not the value is valid, and injects an error message into the model’s .errors collection if it’s not valid. I know that the validator is coupled tightly to my concern’s implementation and data structure. In this case, I’m ok with that. This validator is not meant to be used with any other fields, and is very directly a part of this solution’s implementation detail. The validator is even included automatically, so that I never have to set it up manually inside of my actual model.

 

Mongoid::DateField In Action

Now that I have this in place, my model is reduced to the following:

class SomeModel
  include Mongoid::Document
  include Mongoid::DateField

  date_field :date
end

 

That’s it. My model will now validate any arbitrary input for a date field, in a clean and easily re-usable manner.

For my actual application, here’s what that looks like:

Screen shot 2011 06 13 at 3 35 18 PM

Notice the ‘Start Date’ field on the right hand side. When I fill in this field with something invalid and click save, I get the error message stating that it’s not valid and needs to be in a correct format. The value is also retained on the form so that the person can see what they did wrong.

 

One Remaining Issue: Mass Attribute Updates

Although I’ve solved the majority of the problems I had with this solution, there is one remaining issue: I can’t call .write_attributes or .update_attributes, and by extension, cannot call .create or .new with a hash of values that contains the date fields. Since the solution only provides the parsing and validation during a call to the get and set accessor methods, the parsing and validation doesn’t run and an exception would be thrown for an invalid date.

The workaround here, is that I have to resort to rejecting the values from the form’s params when posting to the server and then manually assign them to the attributes:

med_data = params[:medication]
start_date = med_data[:start_date]
prescription_date = med_data[:prescription_date]

med_data.reject! {|k,v| [:start_date, :prescription_date].include? k.to_sym}

med = Medication.new med_data
med.start_date = start_date
med.prescription_date = prescription_date

med.save

 

It’s a small price to pay for having a generally clean solution. However, I would love to solve this and be able to pass the invalid date strings into .write_attributes without worrying. I would love to see a modification to my solution that allows this to happen… *wink wink nudge nudge* :)


Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Derick Bailey

Derick Bailey is an entrepreneur, problem solver (and creator? :P ), software developer, screecaster, writer, blogger, speaker and technology leader in central Texas (north of Austin). He runs SignalLeaf.com - the amazingly awesome podcast audio hosting service that everyone should be using, and WatchMeCode.net where he throws down the JavaScript gauntlets to get you up to speed. He has been a professional software developer since the late 90's, and has been writing code since the late 80's. Find me on twitter: @derickbailey, @mutedsolutions, @backbonejsclass Find me on the web: SignalLeaf, WatchMeCode, Kendo UI blog, MarionetteJS, My Github profile, On Google+.
This entry was posted in Mongoid, Rails, Ruby. Bookmark the permalink. Follow any comments here with the RSS feed for this post.