Rails: restore ActiveRecord object encoded with to_json - ruby-on-rails

I have a model called Account with these associations:
has_many :contracts, :dependent => :destroy
has_many :packages, :dependent => :destroy
accepts_nested_attributes_for :contracts
accepts_nested_attributes_for :packages
Before destroying any Account object i save it on a file with to_json:
#account.to_json(:include => [:packages, :contracts])
Fine. The problem happens when i try to restore it ( on another script ):
account_data = JSON.parse json
#account = Account.new account_data
This raises an exception:
Package(#70193553579560) expected, got Hash(#70193548333800)
Why this happens? Shouldn't Rails accept a hash in this case?
Will i have to remove contracts and packages keys from Hash and insert them after i do #account.save? I'm looking for a cleaner way to handle this :)

Look at the output of #account.to_json(:include => [:packages, :contracts]). It serializes the associations as JSON, so you end up with something like:
{"id":10, packages:[{id:5,description:"Package1"}], contracts:[]}
When you try to reload the JSON, under the hood it's trying to do this:
account.packages = [{id:5,description:"Package1"}]
This doesn't work because account.packages is an association, and you can't build it directly using a Hash. You can, however, pass the Hash as nested attributes:
account.packages_attributes = [{id:5,description:"Package1"}]
packages_attributes is a method defined by the accepts_nested_attributes_for class method, which you already have in your model.
This is not going to play nicely with to_json. However, ActiveRecord also has a from_json method that is designed to play nicely to to_json

Related

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.

Issue with collection `build` method record instantiation in Rails 4.1.1

I'm having a problem with the Rails collection.build(attrs) method, specifically with how the framework instantiates a new record. For example, here is a much simplified view of my models:
class Document < ActiveRecord::Base
belongs_to :account
has_many :descriptions, before_add: :id_test
validates :account, presence: true
def id_test
puts self.account_id
end
end
When I do something like:
current_account.documents.build(:descriptions => [desc])
then id_test prints nothing. That is, the account_id is not set in the before_add callback (and yes, I've tried the after_add callback as well; account_id is not set in that case either).
If I do:
d = current_account.documents.build
d.assign_attributes(:descriptions => [desc])
Then everything works as expected. However, I would prefer a better alternative, since this would be a pain to implement in the controllers...
Is there a way to get Rails to add the foreign_key first, or is there some better way to set this up? I haven't gone back to check for sure, but this seems different than the way Rails 3 evaluated collection.build statements.
EDIT
Looking through the Rails code a bit, I see I can do:
current_account.documents.build do |record|
record.assign_attributes(:descriptions => [desc])
end
and everything works as expected. Although a bit more verbose, I guess this is technically more accurate anyway.
I would suggest using
Document.build(:account_id => current_account.id, :descriptions => [desc])

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.

How to prevent has_and_belongs_to_many delete option from deleting records that are being used in Rails?

class Assembly < ActiveRecord::Base
has_and_belongs_to_many :parts
end
class Part < ActiveRecord::Base
has_and_belongs_to_many :assemblies
end
In console:
part1 = Part.new
assembly1 = Assembly.new
assembly1.parts << part1
part1.delete
Parts.all
=> []
Checking assembly1.parts shows that there is still a relationship.(!)
How is this possible when the record was deleted?
Also, how to prevent deletion of parts that are associated to assemblies?
Working in Rails 3.0.7.
Everything you were doing here was done from memory (nothing was stored in the database).
the ActiveRecord delete method will remove an object from the database but it doesn't look for other objects in memory that may have already been referencing that object. I think if you did assembly1.parts.delete(part1) that would likely do what you were expecting.
If you had saved the objects to the database:
part1 = Part.create
assembly1 = Assembly.create(:parts => [part1])
assembly1.parts
# => [part1]
part1.delete
assembly1.parts
# => [part1]
assembly1.reload
assembly1.parts
# => []
Note here how even if it's in the database part1.delete won't necessarily remove it from your assembly object until you refresh the in-memory collection or delete it using the method I mentioned earlier assembly1.parts.delete(part1)
UPDATE
I think you usually shouldn't use the delete() method. You should almost always use destroy(). delete() will just fire off a delete to the database and ignores all callbacks and I believe :dependent => :destroy-style declarations in your model. If you use the destroy() method then you can declare a before_destroy callback in your model:
class MyClass
has_and_belongs_to_many :foos
before_destroy :allow_destroy
def allow_destroy
foos.empty?
end
end
That should get your requirement of not destroying it if it is part of an assembly. You cannot stop delete() from executing because it ignores callbacks: ActiveRecord::Relation#delete documentation
More info about model callbacks (documentation)
You need to add :dependent => :destroy to the controller.
You can prevent myobject.delete in the database by adding some referential integrity with a foreign key.
I recommend looking at Adding foreign key to a rails model
in my project this did not prevent myobject.destroy. I think this is because it uses the awesome nested set gem tries really hard to handle cascading destroy for you.
to prevent myobject.destroy
I found How do I 'validate' on destroy in rails supper helpful
I ended up using an before_destroy as well as adding some referential integrity with add_foreign_key in a migration.
this will prevent deletion if myobject.delete is used or if myobject.destroy is used.
in my case myobject hads many of itself and belongs to one of itself. this is what act_as_nested_set handles.
Model
class Myobject
before_destroy :allow_destroy
# ^ this has to be above act_as_nested_set
acts_as_nested_set
def allow_destroy
return true if self.descendants.blank?
# the error is optional.
self.errors.add('Cannot_delete', 'myobject still has children')
throw(:abort)
end
end
Migration
class AddForignKeyToMyobject < ActiveRecord::Migration
def change
add_foreign_key :myobject, :myobject, column: :parent_id
end
end

accepts_nested_attributes_for :reject_if to trigger another method

I've got a multi-level nested form using formtastic_cocoon (jquery version of formtastic).
I am trying to do some validation in the sense of
if value is_numeric do
insert into database
else do
database lookup on text
insert id as association
end
I was hoping tha the accepts_nested_attributes_for would have an :if option, but apparently there is only the :reject_if.
Is there a way to create a validation like I describe as part of the accepts_nested_attributes_for??
-----------------------Updated as per Zubin's Response ---------------------------
I believe Zubin is on the right track with a method, but I can't seem to get it working just right. The method I am using is
def lookup_prereq=(lookup_prereq)
return if lookup_prereq.blank?
case lookup_prereq
when lookup_prereq.is_a?(Numeric) == true
self.task_id = lookup_prereq
else
self.task = Task.find_by_title(lookup_prereq)
end
end
When I trigger this function, the self.task_id is being put in the database as '0' rather than the Task.id.
I'm wondering if I'm missing something else.
I'm not completely sure that the method is actually being called. Shouldn't I need to say
lookup_prereq(attr[:prereq_id)
at some point?
-------------------further edit -----------------------
I think from what I can find that the method is called only if it is named with the same name as the value for the database, therefore I've changed the method to
def completed_task=(completed_task)
Unfortunately this is still resulting in 0 as the value in the database.
Sounds like you need a method in your nested model to handle that, eg:
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :author
def lookup_author=(lookup_author)
return if lookup_author.blank?
case lookup_author
when /^\d+$/
self.author_id = lookup_author
else
self.author = Author.find_by_name(lookup_author)
end
end
end

Resources