POSTing JSON with Association data - ruby-on-rails

I am trying to send a POST request to a rails scaffold controller which contains a nested array representing records that need to be created and associated with the newly created parent.
Here is some example JSON:
{
"plan_id":3,
"weight":60,
"exercise_sets": [
{
"created_at":"2012-06-13T14:55:57Z",
"ended_at":"2012-06-13T14:55:57Z",
"weight":"80.0",
"repetitions":10,
"exercise_id":1
}
]
}
..and my models..
class Session < ActiveRecord::Base
has_many :exercise_sets, :dependent => :destroy
has_many :exercises, :through => :exercise_sets
end
class ExerciseSet < ActiveRecord::Base
belongs_to :exercise
belongs_to :session
end
Is what I am attempting possible?

This is certainly not impossible, though you may have to switch up your parameter naming a bit.
When you pass the JSON above to the controller, it either gets passed as parameters to a constructor:
Session.new(params[:session])
Or gets passed to the #update_attributes method on a persisted Session instance:
#session = Session.find(params[:id])
#session.update_attributes(params[:session])
Both the constructor and #update_attributes methods turn parameters like "plan_id" into assigment method calls. That is,
#session.update_attributes(:plan_id => "1")
Turns into (inside the #update_attributes method):
#session.plan_id = "1"
So, this works for your plan_id and weight attributes, because you have both #plan_id= and #weight= setter methods. You also have an #exercise_sets= method given to you by has_many :exercise_sets. However, the #exercise_sets= method expects ExerciseSet objects, not ExerciseSet attributes.
Rails is capable of doing what you are trying to do via the #accepts_nested_attributes_for class method. Try this:
class Session < ActiveRecord::Base
has_many :exercise_sets, :dependent => :destroy
has_many :exercises, :through => :exercise_sets
accepts_nested_attributes_for :exercise_sets
end
This sets up (metaprograms) an #exercise_sets_attributes= method for you. So just modify your JSON to:
{
"plan_id":3,
"weight":60,
"exercise_sets_attributes": [
{
"created_at":"2012-06-13T14:55:57Z",
"ended_at":"2012-06-13T14:55:57Z",
"weight":"80.0",
"repetitions":10,
"exercise_id":1
}
]
}
More info: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

It's perfectly possible, you just have to add the following line to your Session model:
accepts_nested_attributes_for :exercise_sets, :reject_if => lambda { |a| a[:exercise_id].blank? }, :allow_destroy => true
Now, when your controller does Session.new(params[:session]), Rails will build (or update) the Session and the related ExerciceSet(s).
Please review the :reject_if clause. That's where you define which records will be created and which will not.
This are the basics, but Ryan Bates explains the nested model forms perfectly (as always) in this screencast.

Related

Should accepts_nested_attributes_for also be setting up the association correctly?

Given some kind of Thing:
class Thing < ApplicationRecord
include CustomFieldable
#...
end
Which can have custom field values attached to it:
module CustomFieldable
extend ActiveSupport::Concern
included do
has_many :custom_field_values, as: :custom_fieldable, dependent: :destroy
validates_associated :custom_field_values
accepts_nested_attributes_for :custom_field_values
end
end
And where custom field values are basically just a string value (at least for now) with a reference to their owner:
class CustomFieldValue < ApplicationRecord
belongs_to :custom_fieldable, polymorphic: true, dependent: :destroy
belongs_to :custom_field, dependent: :destroy
validates_presence_of :custom_fieldable
validates_presence_of :custom_field
validates_presence_of :string_value
end
And to the custom field, which is just a wrapper around a name:
class CustomField < ApplicationRecord
validates_presence_of :name
validates_uniqueness_of :name
end
When I initialise the Thing with a hash:
"thing"=>{
//...other stuff...
"custom_field_values_attributes"=>{
"0"=>{
"custom_field_id"=>"1",
"string_value"=>"value 1"
}
}
}
I would expect ActiveRecord to set up the association from the CustomFieldValue back to the Thing. But it looks like it is not, because I get a validation error:
There were problems with the following fields:
Custom field values custom fieldable can't be blank
Custom field values is invalid
So it's like when I use accepts_nested_attributes_for, the parent association is not set up. Is that expected behaviour?
Update #1:
Controller logic for permitting the fields looks like this:
class ThingController < ApplicationController
def thing_params(action)
common_params = [ omitting common stuff... ]
params.fetch(:licence).permit(*common_params,
custom_field_values_attributes: [
:custom_field_id, :string_value ])
end
end
Update #2:
If I write two tests for the model, I can see the same thing happening.
Fails:
test "adding a custom field value on construction via nested attributes" do
thing = Thing.new custom_field_values_attributes: [
{ custom_field_id: custom_fields(:environment).id,
string_value: 'Testing' }
]
assert_attribute_not_invalid thing, :custom_field_values
assert_equal 'Testing', thing.custom_field_values[0].string_value
end
Passes:
test "adding a custom field value via nested attributes" do
thing = things(:one)
thing.update_attributes custom_field_values_attributes: [
{ custom_field_id: custom_fields(:environment).id,
string_value: 'Testing' }
]
assert_valid thing
assert_equal 'Testing', thing.custom_field_values[0].string_value
end
So it's like, if the record isn't saved yet, Rails doesn't set up the nested models correctly, but if it's already saved, they get set up correctly.
I tried something on a whim. Changed this:
has_many :custom_field_values, as: :custom_fieldable, dependent: :destroy
To this:
has_many :custom_field_values, as: :custom_fieldable, inverse_of: :custom_fieldable, dependent: :destroy
So it seems that Rails cannot guess the inverse relationship for polymorphic associations - even though I was already forced to tell it this using :as. Specifying it twice works fine.
With the code given, I would say every thing works fine. You create a Thing object and add multiple CustomFieldValue objects. The new CustomFieldValue objects have set custom_field_id to 1. This does not mean that Rails loads the custom_field object. This would only happen when you save the object and reload it. So validates_presence_of :custom_field is right to complain. This attribute is still nil. I think the same will happen for custom_fieldable

How to "accept" nested class object instances by running a custom method before to store those?

I am using Ruby on Rails 3.1.0 and I would like to run a :reject_if method (as described in the official documentation related to the accepts_nested_attributes_for method in the "One-to-many" association section) on class object instances "at all" (that is, to run the :reject_if method not on attributes - as made in the linked documentation - but on records that should be "subsequently" stored in the database). In few words, I would like to reject\filter some nested model\class object instances that should be stored in the database by running a custom method on those instances.
For example, if I have the following:
class User < ActiveRecord::Base
has_one :article,
accepts_nested_attributes_for :article
end
I would like to make something like this (the following code does not work):
class User < ActiveRecord::Base
has_one :article,
# Note: by using 'article' in the block I (would like to) refer to class
# object instances
accepts_nested_attributes_for :article, :reject_if => { |article| article.can_be_created? }
end
class Article < ActiveRecord::Base
def can_be_created?
true if user.has_authorization?
end
end
Is it possible? If so, how can I make that?
Make a method that returns a boolean, and give the method name as a symbol to :reject_if, eg:
accepts_nested_attributes_for :article, :reject_if => :article_uncreateable?
def article_uncreateable?
!Article.new(article_attributes).can_be_created?
end

has_many association, nested models and callbacks

I've got model A and model Attach. I'm editing my A form with nested attributes for :attaches. And when I am deleting all attaches from A via accepts_nested_attributes_for how can I get after_update/after_save callbacks for all of my nested models? Problem is that when I am executing callbacks in model A they are executed right AFTER model A is updated and BEFORE model Attach is updated, so I can't, for example, know if there is NO ANY attaches after I delete them all :).
Look for example: my callback after_save :update_status won't work properly after I delete all of my attaches.
model A
after_save :update_status
has_many :attaches
accepts_nested_attributes_for :attaches, :reject_if => proc { |attributes| attributes['file'].blank? }, :allow_destroy => true
def update_status
print "\n\nOUPS! bag is empty!\n\n" if self.attaches.empty?
end
end
model Attach
belongs_to A
end
I am using rails 3 beta
From rubyonrails.org:
IMPORTANT: In order for inheritance to work for the callback queues, you
must specify the callbacks before
specifying the associations.
Otherwise, you might trigger the
loading of a child before the parent
has registered the callbacks and they
won‘t be inherited.
Isn't it your problem? You're specifying the association before the callback.
Ok, I've removed after_save callback from A to nested model Attach (after_destroy callback)
model A
has_many :attaches
accepts_nested_attributes_for :attaches, :reject_if => proc { |attributes| attributes['file'].blank? }, :allow_destroy => true
end
model Attach
after_destroy :update_status
belongs_to :a
def update_status
print "\n\nOUPS! bag is empty!\n\n" if self.a.attaches.empty?
end
end

Tell me how to use ActiveRecord#afeter_add

Now I hava a problem,how can I make the callback#after_add receive a reference to the join model in a has_many :through association?
my code like this:
class Emergency
has_many :departments, :through => :eme_references, :after_add => Proc.new { |eme_reference| eme_reference.eme_flag = 1}
end
the attribute eme_flag is the model EmeReference's attribute! but in the block ,i get the eme_reference.class is Emergency.
I want to set the attribute eme_flag of the model EmeReference.
That is my question!
cheers!
Presumably Emergency also has_many :eme_references in order for the :through association to work?
In that case, you should be able to attach the callback there:
has_many :eme_references,
:after_add => Proc.new { |emergency, eme_ref| # code here }
The block accepts 2 parameters, the first will be the Emergency, the 2nd will be the EmeReference being added.
Perhaps a before_save callback on EmeReference can also do what you want in this instance?
I think what you want to do can't be done there.
You could create an after_create hook on departments (I'm assuming Emergency has_many eme_references has_many departments):
class Emergency
has_many :departments, :through => :eme_references
def flag!
eme_flag=1
save
end
end
class Department
after_create :check_emergency
# this allows you to call department.emergency. Will return nil if anything is nil
delegate :emergency, :to =>:eme_reference, :allow_nil => true
def check_emergency
self.emergency.flag! if self.emergency.present?
end
end

Remove assosiation instead of destroying object when :allow_destroy => true

When using the new accepts_nested_attributes_for in ActiveRecord, it's possible to use the option :allow_destroy => true. When this option is set, any hash containing nested attributes like {"_delete"=>"1", "id"=>"..."} passed to update_attributes will delete the nested object.
Simple setup:
class Forum < ActiveRecord::Base
has_many :users
accepts_nested_attributes_for :users, :allow_destroy => true
end
class User < ActiveRecord::Base
belongs_to :forum
end
Forum.first.update_attributes("users_attributes"=>{"0"=>{"_delete"=>"1", "id"=>"42"}})
Question: How do I - instead of deleting the nested objects when "_delete" => "1" - just remove the association? (i.e. In the above case set the forum_id on the user to nil)
Bonus question: What if I also want to change the an attribute on the nested object when removing the association? (e.g. like setting a state or a timestamp)
Instead of asking for the user to be deleted using "_delete" => '1', can you not just update it using the nested_attributes?:
Forum.first.update_attributes("users_attributes"=> {
"0" => {
"id" => "42",
"forum_id" => "",
"state" => 'removed'
}
})

Resources