accepts_nested_attributes_for validation - ruby-on-rails

I'm using Rails 2.3.8 and accepts_nested_attributes_for.
I have a simple category object which uses awesome_nested_set to allow nested categories.
For each category I would like a unique field called code. This would be unique for the category per level. Meaning parent categories will all have unique codes and sub categories will be unique within their own parent category.
EG:
code name
1 cat1
1 sub cat 1
2 cat2
1 sub cat 1
2 sub cat 2
3 cat3
1 sub1
This works without the validation process but when I try and use something like:
validates_uniqueness_of :code, :scope => :parent_id
That will not work because the parent has not been saved yet.
Here is my model:
class Category < ActiveRecord::Base
acts_as_nested_set
accepts_nested_attributes_for :children, :reject_if => lambda { |a| a[:name].blank? }, :allow_destroy => true
default_scope :order => "lft"
validates_presence_of :code, :name, :is_child
validates_uniqueness_of :code, :scope => :parent_id
end
I have thought of a different way to do this and it's very close to working, problem is I cannot check for uniqueness between child categories.
In this second example I've embedded a hidden field in the form called 'is_child' to flag if the item is a sub category or not. Here is my example of this model:
class Category < ActiveRecord::Base
acts_as_nested_set
accepts_nested_attributes_for :children, :reject_if => lambda { |a| a[:name].blank? }, :allow_destroy => true
default_scope :order => "lft"
validates_presence_of :code, :name, :is_child
#validates_uniqueness_of :code, :scope => :parent_id
validate :has_unique_code
attr_accessor :is_child
private
def has_unique_code
if self.is_child == "1"
# Check here if the code has already been taken this will only work for
# updating existing children.
else
# Check code relating to other parents
result = Category.find_by_code(self.code, :conditions => { :parent_id => nil})
if result.nil?
true
else
errors.add("code", "Duplicate found")
false
end
end
end
end
This is very close. If there was a way to detect duplicate codes in the reject_if syntax under accepts_nested_attributes_for then I would be there. This all seems over complicated though and would like suggestions to make it easier. We would like to keep adding categories and sub categories in the one form as it speeds up data entry.
Update:
Maybe I should be using build or before_save.

Instead of
validates_uniqueness_of :code, :scope => :parent_id
Try
validates_uniqueness_of :code, :scope => :parent
Besides that you'll need to set in the Category class:
has_many :children, :inverse_of => :category # or whatever name the relation is called in Child
The use of inverse_of will make the children variable parent be set before saving, and there is a chance it will work.

Related

Rails has_many through cannot save in nested form due to id = null

Using Rails 4.1.13 and Ruby 2.0.0 (although I had the same problem with Ralis 4.0 and Ruby 1.9.3. I have read numerous articles about this particular issue and cannot understand why my solution (which seems exactly like this) does not work, so please help me out.
I have two models BlogPost and Tag. A BlogPost can have many Tags and one Tag can have many BlogPosts. I connect them through a third model BlogPostRelation. Thus, this is my basic setup:
# blog_post.rb
has_many :blog_post_tag_relations, dependent: :destroy
has_many :tags, :through => :blog_post_tag_relations
accepts_nested_attributes_for :blog_post_tag_relations, :tags
# tag.rb
has_many :blog_post_tag_relations, dependent: :destroy
has_many :blog_posts, :through => :blog_post_tag_relations
# blog_post_tag_relation.rb
belongs_to :tag
belongs_to :blog_post
validates_uniqueness_of :tag_id, :scope => [:blog_post_id]
validates :blog_post_id, :presence => true
validates :tag_id, :presence => true
accepts_nested_attributes_for :tag, :blog_post
I have a form for BlogPost, using Formtastic, where I create checkboxes for the BlogPost using:
<%= f.input :blog_title %>
<%= f.input :tags, as: :check_boxes, :collection => tags.order(:name) %>
The problem I have is that BlogPost is not saved before the Tags are added which causes an validation failure of blog_post_id not being present (which it isn't):
Tag Load (1.6ms) SELECT "tags".* FROM "tags" WHERE "tags"."id" IN (678, 56)
(0.9ms) BEGIN
BlogPost Exists (1.6ms) SELECT 1 AS one FROM "blog_posts" WHERE ("blog_posts"."id" IS NOT NULL) AND "blog_posts"."slug" = 'de22323' LIMIT 1
BlogPostTagRelation Exists (1.2ms) SELECT 1 AS one FROM "blog_post_tag_relations" WHERE ("blog_post_tag_relations"."tag_id" = 678 AND "blog_post_tag_relations"."blog_post_id" IS NULL) LIMIT 1
CACHE (0.0ms) SELECT 1 AS one FROM "blog_posts" WHERE ("blog_posts"."id" IS NOT NULL) AND "blog_posts"."slug" = 'de22323' LIMIT 1
BlogPostTagRelation Exists (1.1ms) SELECT 1 AS one FROM "blog_post_tag_relations" WHERE ("blog_post_tag_relations"."tag_id" = 56 AND "blog_post_tag_relations"."blog_post_id" IS NULL) LIMIT 1
CACHE (0.0ms) SELECT 1 AS one FROM "blog_posts" WHERE ("blog_posts"."id" IS NOT NULL) AND "blog_posts"."slug" = 'de22323' LIMIT 1
(0.8ms) ROLLBACK
It seems like the solution should be to use inverse_of, which I frankly don't understand to 100%. It should also be mentioned that I am not 100% sure on how to use accepts_nested_attributes_for either for this type of issue. I have tried all different setups but as far as I understand the only place they should be is in the join model, BlogPostRelation, like this:
# blog_post_tag_relation.rb
belongs_to :tag, :inverse_of => :blog_post_tag_relations
belongs_to :blog_post, :inverse_of => :blog_post_tag_relations
validates_uniqueness_of :tag_id, :scope => [:blog_post_id]
validates :blog_post_id, :presence => true
validates :tag_id, :presence => true
accepts_nested_attributes_for :tag, :blog_post
This doesn't work either and I am completely lost now in what to do.
Most important: What should I do?
Is inverse_of the solution to this problem? If so, how should I use it?
Am I using accepts_nested_attributes_for correctly?
Does it have to do with the naming of BlogPostTagRelation (should it have been called BlogPostTag instead?
Part of the problem here is that you are validating on ids. Rails cannot validate that blog_post_id is present if the id is not known, but it can validate that blog_post is present.
So part of the answer at least is to validate the presence of the associated instance, not the id.
Change the validations to:
validates :blog_post, :presence => true
validates :tag , :presence => true
I would always specify inverse_of as well, but I'm not sure it is part of this problem.
Your model structure is okay.
There's one nifty way you can add tags to your posts after the post is created. You just need to use a model method for doing that.You do not need inverse_of . Here is how:
In your view, add a custom attribute (all_tags).
<%= f.text_field :all_tags, placeholder: "Tags separated with comma" %>
You need to permit the parameter in the controller.
In your Post model add these three methods:
def all_tags=(names)
self.tags = names.split(",").map do |name|
Tag.where(name: name.strip).first_or_create!
end
end
#used when the post is being created. Splits the tags and creates entries of them if they do not exist. `self.tags` ensures that they tags will be related to the post.
def all_tags
self.tags.map(&:name).join(", ")
end
#Returns all the post tags separated by comma
def self.tagged_with(name)
Tag.find_by_name!(name).posts
end
#Returns all the posts who also contain the tag from the current post.
Here's a full implementation
You are using nested_attributes_for correctly, but in this case, you are having models who just have a name and a belongs_to column, so using this is an overkill.
You can call it a tagging, although there's no convention for naming. If you and other can understand it, it is fine.
What you really want to use is a has_and_belongs_to_many (HABTM) association: http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
This however assumes you do not want to do anything with the Relationship Model (blog_post_tag_relations in your case)
You would need only the following models and associations:
class BlogPost < ActiveRecord::Base
  has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
  has_and_belongs_to_many :blog_posts
end
You would then have to rename your join table blog_post_tag_relations to blog_posts_tags, the alphabetical plural combination of the two models. Rails automagically looks up/uses that table seamlessly in the background. It will only have the relationship's foreign_keys:
create_table :blog_posts_tags, id: false do |t|
t.belongs_to :blog_post, index: true
t.belongs_to :tag, index: true
end
Then your form just works:
<%= f.input :blog_title %>
<%= f.input :tags, as: :check_boxes, :collection => tags.order(:name) %>
try
validates :blog_post, :presence => true
validates :blog_post_id, :presence => true, allow_nil: true #can ignore this

How do you write a Rails model method to return a list of its associations?

What I'm trying to do is write a method that will return all of this model's outing_locations, to which it has a has_many relationship.
class Outing < ActiveRecord::Base
attr_accessible :description, :end_time, :start_time, :title, :user_id
belongs_to :user
has_many :outing_locations
has_many :outing_guests
has_one :time_range, :foreign_key => "element_id", :conditions => { :element_type => "outing" }
validates :title, :presence => true
validates :start_time, :presence => true # regex
validates :end_time, :presence => true # regex
def host_name
return User.find(self.user_id).full_name
end
end
I'm trying to get this block in particular to work.
There's another model called OutingInvite, which contains the id of a particular Outing. I need to use that to grab the proper Outing and then pull said Outing's associated outing locations.
Here's a rough sample:
<%#outing_invites.each do |invite|%>
...
<% #party = Outing.where(:id => invite.outing_id) %>
<% #party.outing_locations.each do |location| %>
And then have it output each location.
However, it's saying the method 'outing_locations' does not exist...
You can see a model's associated models by typing model_instance.associated_model_name. So in your example, an outing has_many outing_locations. After you have an instance of an outing, say by using #o = Outing.find(1), you can then use o.outing_locations to see the outing_locations associated with that specific outing.
See this example from the Ruby on Rails Guide.
EDIT
The reasons you're getting the method 'outing_locations' does not exist error is because Outing.where(:id => invite.outing_id) returns an array, and there is no outing_locations method for arrays. You'll need to either get the specific instance (like with Outing.find(invite.outing_id) or use a specific index in that array. I recommend using Outing.find(invite.outing_id) since (I'm assuming) each of your Outing's has a unique id.

accepts_nested_attributes_for with polymorphic association

Is there any way to halt saving of child before parent.
I am using accepts_nested_attributes_for with polymorphic association.
I used multiple options validates_presence_of :parent_id , validates_assoicated :parent but none are working.
For example, I do have a class
Class Person
include HasPhoneNumbers
..
end
module HasPhoneNumbers
def self.included(kclass)
kclass.has_many :phone_numbers, :as => :callable, :dependent => kclass == Person ? :destroy : :nullify
end
klass.accepts_nested_attributes_for :phone_numbers, :reject_if => lambda {|pn| pn.keys.any?{|k| k.to_sym != :id && pn[k].blank?} }
end
class PhoneNumber
belongs_to :callable, :polymorphic => true
end
So while saving person due to validation in person object, it was not saving. However, child(phone_number) was saving. So I need to restrict it to not save child(phone_number) before parent(person) saves.
I did try multiple options using validates_presence_of and validates_associated, but none are working for me.
#person = Person.new(params[:person])
ActiveRecord::Base.transaction do
person.save!
end
Wrapping your saves within a transaction should roll back the phone number save if the person fails validation.
Reference: ActiveRecord Transactions

Polymorphic model question in rails - does it maintain the relationship for you?

Let's say you have a SiteUpdate and a Comment model, and you want to make Comment polymorphic. You make comment hold a "commentable_id" and "commentable_type"...
Here's our comment model:
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
validates_presence_of :content
validates_presence_of :commentable
end
Here is our SiteUpdate:
class SiteUpdate < ActiveRecord::Base
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
has_many :comments, :as => :commentable
validates_presence_of :subject
validates_length_of :subject, :maximum => 80
validates_presence_of :intro
validates_length_of :intro, :maximum => 200
validates_presence_of :text
validates_presence_of :author
scope :sorted, order("site_updates.created_at desc")
end
Does Rails link the commentable to the site_update instance, or do I have to do that manually?
#site_update.comments << Factory.build(:comment, :commentable_id => nil)
#site_update.save
This fails -> it complains that the comment.commentable_id should not be blank (I set this validation in the Comment model).
So do I do this manually, or do I set this up incorrectly?
Or do I just not validate it at all?
I'm making an assumption that your #site_update object is a new object. If so...
There's a somewhat annoying thing about rails associations. You can't really add them before the record is saved.
What's happening is, you have a new site update object without an ID. You build a new comment object for that site update, so it sets the commentable_type to "SiteUpdate", however, there's no ID yet, so it sets the commentable_id to nil. You save, and it bubbles out to save associated objects, but it doesn't set the comment commentable_id to the SiteUpdate ID, because it doesn't exist.
So if you change it around to be :
#site_update.save
#site_update.comments << Factory.build(:comment, :commentable_id => nil)
#site_update.comments.map { |c| c.save }
it should work.
If it's not a new record...it should work as is.

Rails AR validates_uniqueness_of against polymorphic relationship

Is it posible to validate the uniqueness of a child model's attribute scoped against a polymorphic relationship?
For example I have a model called field that belongs to fieldable:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :name, :scope => :fieldable_id
end
I have several other models (Pages, Items) which have many Fields. So what I want is to validate the uniqueness of the field name against the parent model, but the problem is that occasionally a Page and an Item share the same ID number, causing the validations to fail when they shouldn't.
Am I just doing this wrong or is there a better way to do this?
Just widen the scope to include the fieldable type:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :name, :scope => [:fieldable_id, :fieldable_type]
end
You can also add a message to override the default message, or use scope to add the validation:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :fieldable_id, :scope => [:fieldable_id, :fieldable_type], :message => 'cannot be duplicated'
end
As a bonus if you go to your en.yml, and enter:
activerecord:
attributes:
field:
fieldable_id: 'Field'
You are going to replace the default 'subject' that rails add to the errors with the one you specify here. So instead of saying: Fieldable Id has been already taken or so, it would say:
Field cannot be duplicated

Resources