Better validates_associated method for Rails 3? - ruby-on-rails

Rails 3 includes the validates_associated which is automatically called when saving a nested model. The problem with the method is the message is terrible - "Model(s) is invalid"
There have been a few posts attacking this issue for Rails 2:
http://rpheath.com/posts/412-a-better-validates-associated
http://pivotallabs.com/users/nick/blog/articles/359-alias-method-chain-validates-associated-informative-error-message
and there are probably more. It would be great to have a better version as described in these posts that is Rails 3 compatible. The main improvement would be to include why the associated model fails.

On the relationship, you can use :autosave => true instead which will try to save children models when you save the parent. This will automatically run the validations of the children and they will report with proper error messages.
Moreover, if you add a presence validation on the child that the parent must be set, and you construct the child objects through the association, you don't even need the autosave flag, and you get a beautiful error message. For example:
class Trip < ActiveRecord::Base
validates :name, :presence => true
attr_accessible :name
has_many :places, dependent: :destroy, :inverse_of => :trip
end
class Place < ActiveRecord::Base
belongs_to :trip
validates :name, :trip, presence: true
attr_accessible :name
end
Then you can get an nice error message with the following usage scenario:
> trip = Trip.new(name: "California")
=> #<Trip id: nil, name: "California">
> trip.places.build
=> #<Place id: nil, name: nil, trip_id: nil>
> trip.valid?
=> false
> trip.errors
=> #<ActiveModel::Errors:0x00000004d36518 #base=#<Trip id: nil, name: "California">, #messages={:places=>["is invalid"]}>
> trip.errors[:places]
=> ["is invalid"]
I think validates_associated is a relic of the era before autosaving of children and isn't the best way to do things any more. Of course that's not necessarily documented well. I'm not 100% sure that this also applies to Rails 2.3, but I have a feeling it does. These changes came when the nested attributes feature was added (which was sometime in 2.x).
This is a simplified snippet of code from a training project I posted on github.

I was having this problem, and in the end I used the solution given here by Ben Lee:
validates associated with model's error message
Ben says:
You can write your own custom validator, based on the code for the built-in validator.
Looking up the source code for validates_associated, we see that it uses the "AssociatedValidator". The source code for that is:
module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all?
record.errors.add(attribute, :invalid, options.merge(:value => value))
end
end
module ClassMethods
def validates_associated(*attr_names)
validates_with AssociatedValidator, _merge_attributes(attr_names)
end
end
end
end
So you can use this as an example to create a custom validator that bubbles error messages like this:
module ActiveRecord
module Validations
class AssociatedBubblingValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
(value.is_a?(Array) ? value : [value]).each do |v|
unless v.valid?
v.errors.full_messages.each do |msg|
record.errors.add(attribute, msg, options.merge(:value => value))
end
end
end
end
end
module ClassMethods
def validates_associated_bubbling(*attr_names)
validates_with AssociatedBubblingValidator, _merge_attributes(attr_names)
end
end
end
end
You can put this code in an initializer, something like /initializers/associated_bubbling_validator.rb.
Finally, you'd validate like so:
class User < ActiveRecord::Base
validates_associated_bubbling :account
end
NOTE: the above code is completely untested, but if it doesn't work outright, it is hopefully enough to put you on the right track

validates_associated runs the validations specified in the associated object's class. Errors at the parent class level simply say 'my child is invalid'. If you want the details, expose the errors on the child object (at the level of the child's form in the view).

Most of the time validates_existence_of is all I need.

Related

Virtual attributes Rails - Unknown Attribute Error

I'm making a Rails 4 application. Each incident belongs to a user. Each incident has many events. I want that I can create an Incident with current_user.incidents.new and pass it an attribute message which exists in the Event model. On creation, I wish to create a new event with the said message.
Here's my Incident model.
class Incident < ActiveRecord::Base
# The 'incident' model.
# Incidents are created by Users (belongs_to :user)
# Incidents may be public or private.
# Incidents have many events (identified, fixing, etc.)
belongs_to :user
has_many :events
validates :name, presence: true, length: {maximum: 256}
validates_presence_of :component
validates_presence_of :public
validates_presence_of :user_id
attr_accessor :message
validates_associated :message, presence: true
def message
Event.find_by_incident_id(self.id).message
end
def message=(value)
self.events.new(message: value, status: self.status)
end
private
def incident_params
params.require(:incident).permit(:name, :status, :user_id, :message)
end
end
But, when I run #i = Incident.new(message: 'something'), I get ActiveRecord::UnknownAttributeError: unknown attribute 'message' for Incident.
Please help me figure this out.
The problem is that you're passing the values to the default ActiveRecord::new method for Incident, which won't care about your getters, setters and accessors, it goes straight to the columns.
Some people override or adapt the build method that can implement clever logic like you require, without affecting the base initialize method.
Because your Incident model inherits from ActiveRecord::Base, it inherits the constructor. The ActiveRecord constructor only accepts model attributes (db columns) as params. You are trying to call an arbitrary instance method. You could create a class method that creates an instance then adds the message:
def self.create_with_message(msg)
i = Incident.new
i.message(msg)
return(i)
end

Virtual attribute in a Rails app - why is attr_writer not making my input work with my getter and setter methods?

I'm trying to roll my own tagging system. My setup is (at the moment) much like acts_as_taggable_on, with Tags, Taggable objects, and Taggings to relate the one to the other. Taggable is a module, which will be included in Events, Users, and probably a few other kinds of objects that will be taggable. At the moment I'm just trying to hook it up to work with Events.
I'm following Railscast #167.
In the railscast, the virtual attribute tag_names is made accessible with attr_writer :tag_names.
My problem is, I can't get the tag_names field to accept input unless I use attr_accessible :tag_names (ie, 'attr_accessible' instead of 'attr_writer').
when specifying attr_writer :tag_names, I submit the form and get the error: "Can't mass-assign protected attributes: tag_names". When I put attr_accessible :tag_names instead, it seems to work okay, but this is a security issue, right? (Please note: there isn't a tag_names field in the DB for the Event objects.)
And why can't I replicate the Railscast? I'm running Rails 3.2.11, and the Railscast is from 2009, but I can't find anything saying that attr_writer has been replaced with attr_accessible in this later version or anything like that.
Thanks for any help!
The relevant part of my Event form:
<%= f.input :tag_names, label: "Tags (separate by commas)" %>
My Event model:
class Event < ActiveRecord::Base
include Taggable
# Default - order by start time
default_scope :order => 'events.start_time ASC'
belongs_to :creator, :foreign_key => "creator_id", :class_name => "User"
validates_presence_of :creator
(etc)
My Taggable module:
module Taggable
extend ActiveSupport::Concern
included do
has_many :taggings, :as => :taggable
has_many :tags, :through => :taggings
attr_accessible :tag_names
end
def tag(name)
name.strip!
tag = Tag.find_or_create_by_name(name)
self.taggings.find_or_create_by_tag_id(tag.id)
end
def untag(name)
name.strip!
t = Tag.find_by_name(name)
self.taggings.find_by_tag_id(t).destroy
end
# Return an array of tags applied to this taggable object
def tag_list
Tag.joins(:taggings).where(taggings: {taggable_id: self})
end
# Getter method for virtual attribute tag_names
def tag_names
#tag_names || tags.map(&:name).join(', ')
end
# Setter method for virtual attribute tag_names
def tag_names=(names)
#tag_names = names.split(",").map do |n|
Tag.find_or_create_by_name(n.strip)
end
end
end
attr_accessible and attr_writer are two completely different things. The former is a concept pre-Rails 4 where you are whitelisting attributes that are mass-assignable. The latter is creating an instance method on your class that lets you set a value publicly, but not read it.
There are also attr_reader and attr_accessor.
attr_accessor is maybe what you're confusing with attr_accessible. This method is similar to attr_writer, except it provides both a reader and writer method. attr_reader is the opposite of attr_writer in that it gives you an instance method for reading values, but not writing them.

Rails: Relation with class_name does not get saved properly?

I've got a (rather simple) model representing a comment:
class Comment < ActiveRecord::Base
STATES = [:processing, :accepted, :declined]
belongs_to :note
belongs_to :author, :class_name => 'User'
validates_inclusion_of :state, :in => STATES
validates_presence_of :author
default_scope :order => 'created_at DESC'
def initialize( attributes={} )
super(attributes)
self.state ||= 'processing'
end
end
However, everytime I save a comment (with its fields set properly), the author relation always fails to save (well, actually the comment saves successfully, it just leaves out the author...). This goes as far as Comment.first.valid? returning false due to the validation on the author field (Comment.first.author is nil).
My suspicion is that I handle the default value for state-field in a wrong way? If so, how should I set the default value instead?
thx for your help in advance
About the state attribute, it would be better to use an after_initialize callback to set the default instead of overriding the initialize function :
def after_initialize
self.state ||= 'processing'
end
To properly override a function you should pass params and args this way :
def initialize(*args,&block)
super(*args,&block)
#what-you-want-to-execute
end
Notice that there is often a better way than using this !

validates_uniqueness_of in destroyed nested model rails

I have a Project model which accepts nested attributes for Task.
class Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks, :allow_destroy => :true
end
class Task < ActiveRecord::Base
validates_uniqueness_of :name
end
Uniqueness validation in Task model gives problem while updating Project.
In edit of project i delete a task T1 and then add a new task with same name T1, uniqueness validation restricts the saving of Project.
params hash look something like
task_attributes => { {"id" =>
"1","name" => "T1", "_destroy" =>
"1"},{"name" => "T1"}}
Validation on task is done before destroying the old task. Hence validation fails.Any idea how to validate such that it doesn't consider task to be destroyed?
Andrew France created a patch in this thread, where the validation is done in memory.
class Author
has_many :books
# Could easily be made a validation-style class method of course
validate :validate_unique_books
def validate_unique_books
validate_uniqueness_of_in_memory(
books, [:title, :isbn], 'Duplicate book.')
end
end
module ActiveRecord
class Base
# Validate that the the objects in +collection+ are unique
# when compared against all their non-blank +attrs+. If not
# add +message+ to the base errors.
def validate_uniqueness_of_in_memory(collection, attrs, message)
hashes = collection.inject({}) do |hash, record|
key = attrs.map {|a| record.send(a).to_s }.join
if key.blank? || record.marked_for_destruction?
key = record.object_id
end
hash[key] = record unless hash[key]
hash
end
if collection.length > hashes.length
self.errors.add_to_base(message)
end
end
end
end
As I understand it, Reiner's approach about validating in memory would not be practical in my case, as I have a lot of "books", 500K and growing. That would be a big hit if you want to bring all into memory.
The solution I came up with is to:
Place the uniqueness condition in the database (which I've found is always a good idea, as in my experience Rails does not always do a good job here) by adding the following to your migration file in db/migrate/:
add_index :tasks [ :project_id, :name ], :unique => true
In the controller, place the save or update_attributes inside a transaction, and rescue the Database exception. E.g.,
def update
#project = Project.find(params[:id])
begin
transaction do
if #project.update_attributes(params[:project])
redirect_to(project_path(#project))
else
render(:action => :edit)
end
end
rescue
... we have an exception; make sure is a DB uniqueness violation
... go down params[:project] to see which item is the problem
... and add error to base
render( :action => :edit )
end
end
end
For Rails 4.0.1, this issue is marked as being fixed by this pull request, https://github.com/rails/rails/pull/10417
If you have a table with a unique field index, and you mark a record
for destruction, and you build a new record with the same value as the
unique field, then when you call save, a database level unique index
error will be thrown.
Personally this still doesn't work for me, so I don't think it's completely fixed yet.
Rainer Blessing's answer is good.
But it's better when we can mark which tasks are duplicated.
class Project < ActiveRecord::Base
has_many :tasks, inverse_of: :project
accepts_nested_attributes_for :tasks, :allow_destroy => :true
end
class Task < ActiveRecord::Base
belongs_to :project
validates_each :name do |record, attr, value|
record.errors.add attr, :taken if record.project.tasks.map(&:name).count(value) > 1
end
end
Ref this
Why don't you use :scope
class Task < ActiveRecord::Base
validates_uniqueness_of :name, :scope=>'project_id'
end
this will create unique Task for each project.

How to create nested objects using accepts_nested_attributes_for

I've upgraded to Rails 2.3.3 (from 2.1.x) and I'm trying to figure out the accepts_nested_attributes_for method. I can use the method to update existing nested objects, but I can't use it to create new nested objects. Given the contrived example:
class Product < ActiveRecord::Base
has_many :notes
accepts_nested_attributes_for :notes
end
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id, :body
end
If I try to create a new Product, with a nested Note, as follows:
params = {:name => 'Test', :notes_attributes => {'0' => {'body' => 'Body'}}}
p = Product.new(params)
p.save!
It fails validations with the message:
ActiveRecord::RecordInvalid: Validation failed: Notes product can't be blank
I understand why this is happening -- it's because of the validates_presence_of :product_id on the Note class, and because at the time of saving the new record, the Product object doesn't have an id. However, I don't want to remove this validation; I think it would be incorrect to remove it.
I could also solve the problem by manually creating the Product first, and then adding the Note, but that defeats the simplicity of accepts_nested_attributes_for.
Is there a standard Rails way of creating nested objects on new records?
This is a common, circular dependency issue. There is an existing LightHouse ticket which is worth checking out.
I expect this to be much improved in Rails 3, but in the meantime you'll have to do a workaround. One solution is to set up a virtual attribute which you set when nesting to make the validation conditional.
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id, :unless => :nested
attr_accessor :nested
end
And then you would set this attribute as a hidden field in your form.
<%= note_form.hidden_field :nested %>
That should be enough to have the nested attribute set when creating a note through the nested form. Untested.
check this document if you use Rails3.
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#label-Validating+the+presence+of+a+parent+model
Ryan's solution is actually really cool.
I went and made my controller fatter so that this nesting wouldn't have to appear in the view. Mostly because my view is sometimes json, so I want to be able to get away with as little as possible in there.
class Product < ActiveRecord::Base
has_many :notes
accepts_nested_attributes_for :note
end
class Note < ActiveRecord::Base
belongs_to :product
validates_presence_of :product_id unless :nested
attr_accessor :nested
end
class ProductController < ApplicationController
def create
if params[:product][:note_attributes]
params[:product][:note_attributes].each { |attribute|
attribute.merge!({:nested => true})
}
end
# all the regular create stuff here
end
end
Best solution yet is to use parental_control plugin: http://github.com/h-lame/parental_control

Resources