Saving nested model in Rails 4 - ruby-on-rails

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.

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.

Correct way to construct view when using nested attributes

I'm having difficulty understanding how I should build my view to ensure the update of my project model also updates the appropriate related records
I have the following models: Project, User and AuthorisedUser
Each project has a list of authorised users who are permitted access, and the selection of authorised users is made from a drop-down picklist of all users within the project edit view.
I'm keen to make use of as much of the Rails 'magic' as possible, so my understanding is that in order for the project.update method to deal with saving the collection of authorised_users for the project, I need the following associations:
(Project Model)
has_many :authorised_users
has_many :users, through: :authorised_users
accepts_nested_attributes_for :authorised_users
(User Model)
has_many :authorised_users
has_many :projects, through: :authorised_users
(Authorised User Model)
belongs_to :project
belongs_to :user
What I'm having difficulty understanding is how to construct my view such that the authorised users (i.e. those selected from the list of all users) will appear as required in the params presented to the controller- I think I need somehow to include a reference to the AuthorisedUser model, but I've only been able to find examples of this where the fields_for helper is used, for example:
= project_form.fields_for :authorised_users do |auf|
- selected = #project.authorised_users
= auf.label :user_id, 'Authorised Users'
= auf.collection_select('authusers', User.all, :id, :username, {:prompt => 'Blah', :selected => selected}, {multiple: true, class: 'form-control'})
Although this does result in the appearance of authorised_user_attributes within the controller params, this isn't quite right since the block will (obviously) repeat the selection for every authorised user- I just want one selection box to appear, from which selected users can be saved as 'authorised' against the project
This probably isn't as difficult as I seem to be making it, but I'd be grateful for some clarity on the best approach- for example:
Can this be done implicity by Rails as part of the project.update, or must I iterate through the collection of authorised_users in the project controller, manually updating the associated records?
It's easier than what you're trying to do.
You don't need accepts_nested_attributes_for :authorised_users in the Project model, since you want only to update user_ids. Therefore, you don't need fields_for.
- selected = #project.authorised_users
= project_form.label :user_ids, 'Authorised Users'
= project_form.collection_select :user_ids, User.all, :id, :username, {:prompt => 'Blah', :selected => selected}, {multiple: true, class: 'form-control'})
Don't forget to add user_ids: [] to permitted parameters and remove any unused parameters from the first implementation.

Validations that rely on associations being built in Rails

A Course has many Lessons, and they are chosen by the user with a JS drag-n-drop widget which is working fine.
Here's the relevant part of the params when I choose two lessons:
Parameters: {
"course_lessons_attributes"=>[
{"lesson_id"=>"43", "episode"=>"1"},
{"lesson_id"=>"44", "episode"=>"2"}
]
}
I want to perform some validations on the #course and it's new set of lessons, including how many there are, the sum of the lessons' prices and other stuff. Here's a sample:
Course Model
validate :contains_lessons
def contains_lessons
errors[:course] << 'must have at least one lesson' unless lessons.any?
end
My problem is that the associations between the course and the lessons are not yet built before the course is saved, and that's when I want to call upon them for my validations (using course.lessons).
What's the correct way to be performing custom validations that rely on associations?
Thanks.
looks like you don't need a custom validation here, consider using this one:
validates :lessons, :presence => true
or
validates :lessons, :presence => {:on => :create}
You can't access the course.lessons, but the course_lessons are there, so I ended up doing something like this in the validation method to get access to the array of lessons.
def custom validation
val_lessons = Lesson.find(course_lessons.map(&:lesson_id))
# ...
# check some things about the associated lessons and add errors
# ...
end
I'm still open to there being a better way to do this.

How to bulk validate association in Rails

I have the following scenario:
One of my models, let's call it 'Post', has multiple associated models, Images.
One, and only one, of those images can be the key Image to its Post (that is represented as a boolean flag on the Image model and enforced through a validation on the Image model which uses the Post as its scope).
Now of course when I want to update the primary Image flag, it happens that an Image model's key flag is set to true and the validation fails because there's still another Image with the key flag set to true.
I know, that thing screams to be transformed into an association on the Post model, which links to the key Image, but is there a way to validate associations in bulk in Rails?
What would be your take, would you make the key Image a separate association on the Post model or would you use the boolean flag?
there is a simple solution but it needs some trust:
Remove the validation "is there only one primary image?"
Make sure there will be only one primary image by adding a filter
The big plus is that you don't have to check anything in your controller or post model. Just take an image, set is_primary to true and save it.
So the setup could look like:
class Post < ActiveRecord::Base
has_many :images
# some sugar, #mypost.primary_image gets the primary image
has_one :primary_image,
:class_name => "Image",
:conditions => {:is_primary => true }
end
class Image < ActiveRecord::Base
belongs_to :post
# Image.primary scopes on primary images only
scope :primary, where(:is_primary => true)
# we need to clear the old primary if:
# this is a new record and should be primary image
# this is an existing record and is_primary has been changed to true
before_save :clear_primary,
:if => Proc.new{|r| (r.new_record? && r.is_primary) || (r.is_primary_changed? && r.is_primary) }
def clear_primary
# remove old primary image
Image.update_all({:is_primary => false}, :post_id => self.post_id)
end
end
Edit:
This will work in any case - why?
before_save is only invoked if all validations succeed
the whole save is wrapped in a transaction, this means if clear_primary or the saving of the image itself fails, everyhing will be rolled back to it's original state.
Well you can do this within your Post model:
# Post.rb
has_many :images, :conditions => ['primary = ?', false]
has_one :primary_image, :conditions => ['primary = ?', true]
When you want to change the primary image, do something like this:
# Post.rb
def new_primary_image(image_id)
primary_image.primary = false
Image.find(image_id).primary = true
end

associate nested attributes with the ids they get after save

class Member < ActiveRecord::Base
has_many :posts
accepts_nested_attributes_for :posts
end
params = { :member => {
:name => 'joe', :posts_attributes => [
{ :title => 'Kari, the awesome Ruby documentation browser!' },
{ :title => 'The egalitarian assumption of the modern citizen' },
]
}}
member = Member.create(params['member'])
After doing this I want is to map the elements in posts_attributes in params hash to the id's (The primary keys) after they are saved. Is there any thing I can do when accepts_nested_attributes_for builds or creates each record
PS: posts_attributes array may not contain index in sequence I mean this array might not contain index like 0,1,2 it can contain index like 0,127653,7863487 as I am dynamically creating form elements through javascript
also, I want is to associate only new records created in Post and not already existing Post
Thanks in Advance
Have you considered refreshing the posts association and grabbing the posts_attributes array in full?
Unfortunately, there is not a reliable way to do what you want. You could try looping over both and finding the IDs associated with the content using string matching, but without a field on the posts that is guaranteed to be a unique value, there's not an effective way to do it.
Although I'm not quite sure about what elements you want to assign with what ids, I think this approach would give you a hint.
You may assign a method name symbol to :reject_if, then put your logic into that method, like this:
class Member < ActiveRecord::Base
has_many :posts
accepts_nested_attributes_for :posts, :reject_if => :reject_posts?
def reject_posts?(attrs)
# You can do some assignment here
return true if attrs["title"].blank?
post_exist = self.posts.detect do |p|
p.title == attrs["title"]
end
return post_exist
end
end

Resources