Rails 3 - reject_if proc not catching blank fields - ruby-on-rails

I'm getting rejected by reject_if.
The Item model has_many variants, with the model also accepting nested attributes:
accepts_nested_attributes_for :variants, :allow_destroy => :true,
:reject_if => :all_blank
When I submit, it posts the following parameters:
Parameters: {"utf8"=>"✓", "authenticity_token"=>"66areo4waM82H66771RkUD/Zt3rrp8Hgk/mwOqV42YI=", "item"=>{"name"=>"Dans", "body"=>"adsdsa", "visible"=>"1", "sellable"=>"0", "variants_attributes"=>{"0"=>{"name"=>"", "price"=>"", "qty"=>"", "sku"=>"", "_destroy"=>"false"}}}, "commit"=>"Save Item", "id"=>"6"}
For reference the controller:
def edit
#item = Item.find(params[:id])
#item.variants.build
The variants attributes are blank, but they aren't being rejected by the item model. So all the validations come through, making it unable to save. Any ideas?

Calling build on association doesn't have anything to do with reject_if options of accepts_nested_attributes_for. You call build without any parameters so it initializes variant with default attributes values.
Using reject_if matter when you initialize or update your parent model like that passing attributes for association models (variants) directly to parent model (item):
item = Item.new :name => "Dans", :variants_attributes => { "0" => { :name => "" } }
If reject_if is false you'll get new item with one variant having empty name. If reject_if is true this variant won't be created as all attributes passed are blank. But using this option do not prevent you from creating variants with blank attributes manually using build or directly adding variants to the item like that:
item.variants << Variant.new
Btw, why do you call build in your edit action? Usually edit action just fetches the model and renders the form. Updating attributes of a model happens in update action like that:
#item = Item.find params[:id]
#item.update_attributes params[:item]

Related

Why the new data are not saved in the nested form?

In ActiveAdmin I have an entity editing form with triple nesting. Now I can edit the data that is present in the database. They are saved.
But if I try to add new data, then I get a ROLLBACK error:
{:"blocks.texts.block"=>["must exist", "can't be blank"]}
I'll clarify again - existing data in this field is successfully updating.
But when creating a new entity in this nested form, some kind of problem arises. I tried to track by logs what is sent in the form, what comes before validation and what remains after validation.
Everything comes to form:
"blocks_attributes"=>{"0"=>{"texts_attributes"=>{"0"=>{"value"=>"first value", "_destroy"=>"0", "id"=>"671518"}}, "label_ids"=>["", "54"], "_destroy"=>"0", "id"=>"18655"}, "1"=>{"texts_attributes"=>{"0"=>{"value"=>"tteesstt"}}}}
# => "1"=>{"texts_attributes"=>{"0"=>{"value"=>"tteesstt"}}}
But before and after validation, this data is no longer available. In texts are present only data previously existed.
In ActiveAdmin have this code:
permit_params :title, :description, :published,
blocks_attributes: [
:id, :_destroy,
texts_attributes: %i[id value _destroy],
label_ids: []
],
category_ids: []
# ...
f.has_many :blocks, allow_destroy: true do |b_f|
b_f.inputs do
b_f.has_many :texts, allow_destroy: true do |b_t_f|
b_t_f.inputs do
b_t_f.input :value
end
end
b_f.input :labels, as: :check_boxes, collection: Label.options_for_select, input_html: { multiple: true }
end
end
The initial Post model has this code:
accepts_nested_attributes_for :blocks,
allow_destroy: true
In Block model:
accepts_nested_attributes_for :texts,
allow_destroy: true
Please tell me why the existing data is updated, and the new ones disappear when saved?
Addition 1
As I understand it, this is connected not with texts, but with block - blocks.texts.block. But why does the text refer to a block? Why is the block not identifiable? It has the following name in the form: post[blocks_attributes][1][texts_attributes][0][value].
Addition 2
If in ActiveAdmin I first add (save to DB) only block (second block), and after I add text to this block, all two times the save to DB will successfully. That is, the problem is due to the lack of a block ID when creating text in a single scenario.
It turns out that this is a bug? When adding (using JS) a new HTML form code, must also add the block_id for text. But now this is not. Now only the existing block in the database has this field.
I remember that some time ago I had a similar issue with associations. Here, form error it looks like texts have no block_id. That's true because you're already saving it. Try that: https://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html
The solution is simple - need to use inverse_of. Documentation.
And everything will start to work as intended.

Validating presence of nested attributes returns error "no method :path_base"

I have a model which accepts nested attributes. There are 4 attributes altogether and I need to verify the presence of one. the specific attribute I need to verify for is called path_base so I tried
validates_presence_of :path_base
In the model but I am getting the error
undefined method `path_base' for #<Template:0x007fa279146360>
When saving the template record. The params getting sent look like this
Parameters: {"utf8"=>"✓", "authenticity_token"=>"ZO+Pi3/6WwNk0H3cFhgDbRywjrAOv2RnZ7olIsenND0=", "already_saved"=>"false", "update_pages"=>"false",
"template"=>{"type"=>"singleton", "name"=>"test",
"template_responses_attributes"=>{"0"=>{"path_base"=>"", "liquid_code"=>"test", "indexable"=>"1", "content_type"=>"text/html"}, "1"=>{"path_base"=>"", "liquid_code"=>"", "indexable"=>"1", "content_type"=>"text/html"}},
"template_fields_json"=>"[\r\n\r\n]"}, "button"=>""}
So inside the template_responses_attributes array is where the value of path_base is, and that is inside the template array just like normal (template is the controller/model that is saving the record that accepts the nested attributes).
If anyone could point me in the correct direction for this it would be greatly appreciated.
I did try this, which I found here but it did not return an error when the value was empty.
reject_if: proc { |attributes| attributes['path_base'].blank? }
Each model should only be responsible for validating its own attributes - if you want to ensure that the nested records are valid use validates_associated.
class Template < ApplicationRecord
has_many :responses
accepts_nested_attributes_for :responses
# This validates all the associated records
validates_associated :responses
end
class Response < ApplicationRecord
validates_presence_of :path_base
# ...
end
The reject_if option is not a validation mechanism. Rather it lets you filter out nested attributes if they do not meet a criteria, take for example a task list application where you would want to filter out the empty rows.

Using accepts_nested_attributes_for outside of proper restful routes

I am building a setup wizard for a model called Project. This model has a lot of associated information, which includes a number of nested models.
After some research and a fair bit of trial and error, I decided to manage the setup process in a SetupController, using the :id parameter to track which step I'm on, resulting in a path pattern like so: projects/:project_id/setup/:id/edit (based on this blog)
Here are the relevant bits:
Project Model
class Project < ActiveRecord::Base
has_many :ratings
accepts_nested_attributes_for :ratings, allow_destroy: true, reject_if: -> x { x[:value].blank? }
end
Rating Model
class Rating < ActiveRecord::Base
# has a null: false constraint on value
belongs_to :project
end
Setup Controller
ProjectSetupController < ApplicationController
STEPS = %w(step_1 step_2 step_3)
layout 'setups'
def edit
#project.ratings.build
render step
end
def update
if #project.update_attributes(project_params)
if next_step && params[:button].downcase.include?('continue')
redirect_to edit_project_setup_path(#project, next_step), flash: {success: "Updated project"}
else
redirect_to project_path(#project)
end
else
flash.now[:error] = "Please complete all required fields"
render step
end
end
private
def step
STEPS.find {|s| s == params[:id].to_s.downcase}
end
def current_step_index
STEPS.index(step)
end
def next_step
STEPS[current_step_index+1]
end
def project_params
params.require(:project).permit(:name, ratings_attributes: [:id, :value, :_destroy])
end
end
And this is all well and good, except when it comes to nested attributes. A Project accepts_nested_attributes_for Ratings, but rejects any ratings with blank values. I want the user to be able to submit a form with blank values because multiple rating fields can be added dynamically to the project form and there will always be an empty new field, I just don't want any record without a value to be saved. However, something gets muddled when using the :id parameter as something other than the id of the parent model, and these records are not discarded when the form is submitted. Instead, they hit the Rating database validation for presence of value and an error is thrown.
Form
= simple_form_for #project, url: project_setup_path(#project, params[:id]), as: :project, html: {id: 'customization-form'} do |f|
- #project.ratings.each do |rating|
.rating-wrapper{class: rating.new_record? && "new"}
= f.fields_for :ratings, rating do |ff|
= ff.input_field :value, placeholder: "Enter New Rating"
= button_tag(type: 'submit') do
Update
If I mock a submission with the params[:id] as the id of the project I'm submitting the form for, then everything works as expected (of course this results in a redirect error as the project id is not a valid step), so I feel like there must be some way to point the attributes to the correct id, alas, this magic is beyond me.
Current possible workarounds:
I can submit the form to the regular project controller action with a
button parameter that will redirect the user back into the setup
process
I could remove the empty value fields from the DOM via javascript on
submission
If I remove the Rating validations, I can submit the form as
is, and all blank ratings will be saved, but I could delete them in a
callback
Currently I'm employing the first workaround, but is there a more Rails-y solution that allows me to keep this process within the setup controller, without removing database validations or using javascript? The blog article I modeled my wizard after suggested sub-models for handling interim validations - I don't think that's exactly what I'm looking for here, but maybe there's a way I could leverage something like that?

Saving nested model in Rails 4

Kinda new to the Rails thing, in a bit of a spot.
One of the models is dependent on the other in a has_many/belongs_to association.
Basically, when creating a "Post" on my application, a user can also attach "Images". Ideally these are two separate models. When a user chooses a photo, some JavaScript uploads it to Cloudinary and the returned data (ID, width, height, etc) are JSON stringified and set on a hidden field.
# The HTML
= f.hidden_field :images, :multiple => true, :class => "image-data"
# Set our image data on the hidden field to be parsed by the server
$(".image-data").val JSON.stringify(images)
And of course, the relationship exists in my Post model
has_many :images, :dependent => :destroy
accepts_nested_attributes_for :images
and my Image model
belongs_to :post
Where I'm lost is what to do with the serialized image data on the Post controller's create method? Simply parsing the JSON and saving it doesn't create the Image models with the data upon saving (and doesn't feel right):
params[:post][:images] = JSON.parse(params[:post][:images])
All of this essentially culminates to something like the following parameters:
{"post": {"title": "", "content": "", ..., "images": [{ "public_id": "", "bytes": 12345, "format": "jpg"}, { ..another image ... }]}}
This whole process seems a little convoluted -- What do I do now, and is there a better way to do what I'm trying to do in the first place? (Also are strong parameters required for nested attributes like this...?)
EDIT:
At this point I got this error:
Image(#91891690) expected, got ActionController::Parameters(#83350730)
coming from this line...
#post = current_user.reviews.new(post_params)
Seems like it's not creating the images from the nested attributes but it's expected to.
(The same thing happens when :autosave is there or not).
Just had this issue with that ActionController::Parameters error. You need to make sure you're permitting all the necessary parameters in your posts_controller, like so:
def post_params
params.fetch(:post).permit(:title, :content,
images_attributes: [:id, :public_id, :bytes, :format])
end
It's important to make sure you're permitting the image.id attribute.
You must build the params like this:
params[:post][:images_attributes] = { ... }
You need *_attributes on a key name of images.
The accepts_nested_attributes_for should care of this for you. So doing a Post.create(params[:post]) should also take care of the nested image attributes. What might be going wrong is that you have not specified an autosave on the has_many relationship. So you might want to see if this makes a difference:
has_many :images, :dependent => :destroy, :autosave => true
That should save the images too when you save your post.

Rails 3: How to enforce model's validation to fail when the validation of the associated model fails?

I have the following two models:
class Product < ActiveRecord::Base
belongs_to :shop
validates_numericality_of :price, :greater_than_or_equal_to => 0
end
class Shop < ActiveRecord::Base
has_many :products
validates_presence_of :name
end
Here is the create method of my ProductsController:
def create
if params[:product][:shop_id] == "new_shop"
#shop = Shop.find_by_name(params[:new_shop]) || Shop.create(:name => params[:new_shop]) # Is there a simpler method to do this ?
params[:product][:shop_id] = #shop.id
end
#product = Product.new(params[:product])
if #product.save
redirect_to(:action => 'index')
else
render('new')
end
end
When user adds a new product he has a select box to choose the shop. The last option in this select box lets user to add a new shop (an additional input text field appears). The value of this last option is new_shop.
If the validation of the new entered shop fails, I would like the validation of the product to fail and display an appropriate error (currently an error displayed only if the validation of the product itself fails).
What would be the most "Rails 3 method" to achieve this ?
I think it would be simpler if you use accepts_nested_attributes_for. So to your Product model add:
accepts_nested_attributes_for :shop
And then in view depending on your select list value you can modify form (in js), so there will be either shop_id field or a whole set of fileds for a shop:
<% f.fields_for :shop do |sf| %>
...
<% end %>
Then if user selects existing shop, it will only pass shop_id, but if users selects new shop, then form will pass also new associated object.
If you want shop name to be unique, then just add validates_uniqueness_of to Shop model.
If validation of a shop fails, then product won't be saved. Basicaly, your controller stays as simple as it could be (just creating new product object from params - you don't care about shop there).
I agree with #klew, you should probably be using accepts_nested_attributes_for.
But, the simple and direct answer to your question is to use validates_associated.
Also, the nicer way of doing:
#shop = Shop.find_by_name(params[:new_shop]) || Shop.create(:name => params[:new_shop])
would be:
#shop = Shop.find_or_create_by_name(params[:new_shop])

Resources