RoR Validating dependent models but not stopping parent if dependent validation fails - ruby-on-rails

This is a frustratingly simple problem, but it has vexed me.
I have two models, comments and notes. A comment can have one or no notes. Notes have only a text field. They have a shared form with an accepts_nested_attributes_for field.
At the moment, every time a comment is made, it creates an associated blank note. I only want the note to be created if something is entered in the note's text field. I don't want a gagillion blank notes.
I suspect this is a trivial problem, but I am damned if I can solve it.
I tried validates :text, presence: true, on note but, when it fails, it prevents the parent comment from being created, which is not what is wanted. Grrr.
note.rb
class Note < ApplicationRecord
belongs_to :comment
validates :text, presence: true
comment.rb
class Comment < ApplicationRecord
...
has_one :note, dependent: :destroy
accepts_nested_attributes_for :note
comment/_form.html.erb
<%= form.fields_for :note do |note_form| %>
Notes<br />
<%= note_form.text_area :text, cols: 57, rows: 8 %>
<% end %>`
It is, I guess, doing what it is supposed to do. I just don't want it to do that...
A virtual pint to anyone who can help.

New answer.
I had a look at your issue and turns out that accepts_nested_attributes_for [NOTE the link is for rails 6, but its same with rails 5] expects additional parameters. One of which is reject_if, that means you can pass a proc and if it returns false, the nested record will not save.
So, in your case, you can do the followings
# app/models/note.rb
class Note < ApplicationRecord
belongs_to :comment, optional: true
end
# app/models/comment.rb
class Comment < ApplicationRecord
has_one :note, dependent: :destroy
accepts_nested_attributes_for :note, reject_if: proc { |attributes| attributes['text'].blank? }
end
Note that,
accepts_nested_attributes_for :note, reject_if: proc { |attributes| attributes['text'].blank? }
which will return false if the text on note is blank and in turn avoid saving the blank note record.
I've had a quick test and this works in my rails 5.2 app. \o/
You can refer my bare minimum rails app on github if need be sameera207/so-question-58599317
Initial answer, doesn't work
Assuming this is rails 5, Try :inverse_of
UPDATE: optional: true added after the comment by the OP
class Note < ApplicationRecord
belongs_to :comment, inverse_of: :notes, , optional: true
validates :text, presence: true
class Comment < ApplicationRecord
...
has_one :note, dependent: :destroy, inverse_of: :comment
accepts_nested_attributes_for :note
This is a very good writeup about :inverse_of and it talks about accepts_nested_attributes_for, which I think the issue you are having

Related

How to validate person's address only if person doesn't belong to company?

In my Rails 5 app I have the following setup:
class Client < ApplicationRecord
has_one :address, :as => :addressable, :dependent => :destroy
accepts_nested_attributes_for :address, :allow_destroy => true
end
class Company < Client
has_many :people
end
class Person < Client
belongs_to :company
end
class Address < ApplicationRecord
belongs_to :addressable, :polymorphic => true
validates :city, :presence => true
validates :postal_code, :presence => true
end
A person can belong to a company but doesn't necessarily have to.
Now I want to validate a person's address only if that person doesn't belong to a company. How can this be done?
There might be other approaches as well, but based on my experience, something like this should work.
validates :address, :presence => true, if: -> {!company}
Hope this helps.
Validations can take either an if or unless argument, which accept a method, proc or string to determine whether or not to run the validation.
In your case:
validates :address, presence: true, unless: :company
Update according to comments
The above only takes care of skipping the validation itself, but due to accepts_nested_attributes_for OP still saw errors when trying to persist a missing address. This solved it:
accepts_nested_attributes_for :address, reject_if: :company_id
Nabin's answer is good but wanted to show another way.
validate :address_is_present_if_no_company
def address_is_present_if_no_company
return if !company_id || address
errors.add(:address, "is blank")
end

Validate associated object (lazy validation)

I'm trying to solve validation of associated object with condition.
User doesn't need to have filled author_bio until he is author. So app needs to ensure, that author can't create post without author_bio and author_bio can't be deleted if user already created any post.
class User < ApplicationRecord
has_many :posts, foreign_key: 'author_id', inverse_of: :author
validates :author_bio, presence: { if: :author? }
def author?
posts.any?
end
end
class Post < ApplicationRecord
belongs_to :author, class_name: 'User', inverse_of: :posts, required: true
end
Unfortunately this doesn't validate author on creation of new post:
user = User.first
user.author_bio
=> nil
post = Post.new(author: user)
post.valid?
=> true
post.save
=> true
post.save
=> false
post.valid?
=> false
So how can I prevent creating of new post by user without author_bio? I can add second validation to Post model, but this is not DRY. Is there any better solution?
The answer here seems to be use of validates_associated once you have your associations correctly set up (including inverse_of which you have, but stating for others, rails in many cases misses them or creates them incorrectly)
so to adjust the classes here:
class User < ApplicationRecord
has_many :posts, foreign_key: 'author_id', inverse_of: :author
validates :author_bio, presence: { if: :author? }
def author?
posts.any?
end
end
class Post < ApplicationRecord
belongs_to :author, class_name: 'User', inverse_of: :posts
validates :author, presence: true
validates_associated :author
end
Now when you try running what you did before:
user = User.first
user.author_bio
=> nil
post = Post.new(author: user)
post.valid?
=> false
post.save
=> false
Does not allow you to save since author_bio is empty
Only thing to watch out there was to set up correct associations, otherwise rails was confused and skipping validation on User class since it thought the relationship is not yet in existence.
NOTE: I removed required: true from belongs_to since in rails 5 is default, therefore you won't need validates :author, presence: true either only in rails 5.

one-to-many through referance table

I'm trying to understand how to implement one-to-many relationship through reference table. I'm looking on this guide I though just write on one model has_many so it will be one-to-many but I'm not completely sure (I wrote something but it's not working). Anyway I'm doing this to save for me a table, and doing it right and not just working.
The model is as following:
Microposts, :id, :content
Tag, :id, :name
Tag_microposts, :tag_id, :micropost_id
Article, :id, :text
Article_microposts, :article_id, :micropost_id
I can do two microposts tables with the id of the tag/article. But I think doing it like this is better and righter.
In the end what's interesting me is to get microposts through tag model. So in the tag_controller be able to do:
def index
#tag = Tag.find(params[:id])
#microposts = #tag.microposts
end
Some code:
class Tag < ActiveRecord::Base
...
has_many :tag_microposts, foreign_key: :tag_id, dependent: :destroy
has_many :microposts, through: :tag_microposts, source: :micropost
...
end
class TagMicropost < ActiveRecord::Base
validates :tag_id, presence: true
validates :micropost_id, presence: true
end
class Micropost < ActiveRecord::Base
belongs_to :user
belongs_to :tag
validates :content, presence: true, length: {minimum: 10, maximum: 250}
validates :user_id, presence: true
end
May I ask why you are using a reference table for this? You can do a one-to-many association with only the two models you are associating. If you want to associate a tag with many posts you can just do this in your models.
class Tag < ActiveRecord::Base
has_many :microposts
end
class Micropost < ActiveRecord::Base
belongs_to :tag
#added in edit
belongs_to :article
validates :content, presence: true, length: {minimum: 10, maximum: 250}
validates :user_id, presence: true
end
This should let you do:
#tag.microposts
Just fine. The forgien key will be stored in your Micro post model so be sure to add the column. You can use an active migration for that. Call the column tag_id, rails should take care of the rest.
Edit*
A added the article association. The problem you raised is only relevant if you need to get the article/tag given the micropost. The code to do that is still pretty simple with this model.
#tag ||= #micropost.tag
Using the conditional assignment operator like this will only assign #tag if the association is there. If you give me more specifics about how these models will be used I can give you a better answer.

Rails polymorphic has_one build

Given a ContentBlock model:
class ContentBlock < ActiveRecord::Base
has_one :block_association
has_one :image, through: :block_association, source: :content, source_type: "Image"
has_one :snippet, through: :block_association, source: :content, source_type: "Snippet"
accepts_nested_attributes_for :image, allow_destroy: true
accepts_nested_attributes_for :snippet, allow_destroy: true
end
BlockAssociation model:
class BlockAssociation < ActiveRecord::Base
belongs_to :content_block
belongs_to :content, polymorphic: true
end
Snippet model:
class Snippet < ActiveRecord::Base
has_one :block_association, as: :content
has_one :content_block, through: :block_association
validates :body, presence: true
end
I need to do:
#content_block.build_snippet
but this gives:
undefined method 'build_snippet' for #<ContentBlock:0x007ffb7edde330>
How would I achieve the intended result?
The form would be something like this:
<%= simple_form_for #content_block do |f| %>
<%= f.simple_fields_for f.object.snippet || f.object.build_snippet do |sf| %>
<%= sf.input :body %>
<% end %>
<% end %>
(Originally I had assumed that content_block would simply belong_to :content, polymorphic: true but that seemed inadequate due to the multiple content types.)
This is kind of close to what I'm doing, but I just can't quite get my head around it: http://xtargets.com/2012/04/04/solving-polymorphic-hasone-through-building-and-nested-forms/
class ContentBlock < ActiveRecord::Base
has_one :snippet, through: :block_association, source: :content, source_type: "Snippet"
end
This tells rails, you want instances of ContentBlock (lets make content_block this instance) to have one instance of Snippet called "snippet" of the fake-type "content" through BlockAssociation. ContentBlock instances should hence be able to respond to content_block.content which would retourn a collection of a snippet and/or image (I left out the image part in the code-snippets). How content_block can call only snippet content no one knows yet.
What does your BlockAssociation Model know:
class BlockAssociation < ActiveRecord::Base
belongs_to :content_block
belongs_to :content, polymorphic: true
end
It knows it belongs to a content_block and knows (since it will respond to content) one or more contents which have a content_type ('Snippet') and a content_id (1 or whatever snippets id is), the combination of those makes the relation to snippet
Now what you were missing was the Snippet part:
class Snippet < ActiveRecord::Base
has_one :block_association, :as => :snippet_content
has_one :content_block, :through => :content_association # I'm actually not quite sure of this
end
Which tells block_association how to call this type of content, since you want to differ between image-content and snippet-content. Now content_block.snippet_content should return the snippet and snippet.block_content should return block_content.
I hope I didn't mess anything up, these relations allways get my head spinning

Trouble with accepts_nested_attributes_for on validating foreign key

I am using Ruby on Rails v3.2.2. I would like to solve the issue related to the validation of a foreign key when using accepts_nested_attributes_for and validates_associated RoR methods. That is, I have following model classes:
class Article < ActiveRecord::Base
has_many :category_associations, :foreign_key => 'category_id'
accepts_nested_attributes_for :category_associations, :reject_if => lambda { |attributes| attributes[:category_id].blank? }
validates_associated :category_associations
end
class CategoryAssociation < ActiveRecord::Base
belongs_to :article, :foreign_key => 'article_id'
belongs_to :category, :foreign_key => 'category_id'
validates :article_id, :presence => true
validates :category_id, :presence => true
end
... and I have following controller actions:
class ArticlesController < ApplicationController
def new
#article = Article.new
5.times { #article.category_associations.build }
# ...
end
def create
#article = Article.new(params[:article])
if #article.save
# ...
else
# ...
end
end
end
With the above code ("inspired" by the Nested Model Form Part 1 Rails Cast) my intent is to store category associations when creating an article (note: category objects are already present in the database; in my case, I would like just storing-creating category associations). However, when I submit the related form from the related view file, I get the following error (I am logging error messages):
{:"category_associations.article_id"=>["can't be blank"], :category_associations=>["is invalid"]}
Why it happens since validates_associated seems to run the method article.category_association.valid? but only if the article.category_association.article_id is not nil? How can I solve the problem with the presence validation of the article_id foreign key?
However, if I comment out the validates :article_id, :presence => true in the CategoryAssociation model class, it works as expected but it seems to be not a right approach to do not validate foreign keys.
If I comment out the validates_associated :category_associations in the Article model class, I still get the error:
{:"category_associations.article_id"=>["can't be blank"]}
Use inverse_of to link the associations and then validate the presence of the associated object, not the presence of the actual foreign key.
Example from the docs:
class Member < ActiveRecord::Base
has_many :posts, inverse_of: :member
accepts_nested_attributes_for :posts
end
class Post < ActiveRecord::Base
belongs_to :member, inverse_of: :posts
validates_presence_of :member
end
Since you have a possible nested form with accepts_nested_attributes_for, therefore in CategoryAssociation you need to make the validation conditional, requiring presence for only for only updates:
validates :article_id, presence: true, on: :update
Aside from Active Record associations, you should have a foreign key constraints at the db level.
If you're stucked with this kind of errors too, try to replace:
validates :article_id, :presence => true
validates :category_id, :presence => true
with:
validates :article, :presence => true
validates :category, :presence => true
worked for me.
Validations will run on create or save (as you'd expect), so ask yourself, "at each one of those is there a saved instance being referred to?", because without a save an instance won't have an id as it's the database that assigns the id.
Edit: Like I've said in the comments, if you're going to downvote then leave a comment as to why.

Resources