Update has_many attributes with irregular params - ruby-on-rails

I have model LoanPlan and Career, they are associated by a join_table
The request params from another frontend developer will be like this
"loan_plan" => {
"id" => 32,
"careers" => [
[0] {
"id" => 8,
},
[1] {
"id" => 9,
}
]
},
However, I got ActiveRecord::AssociationTypeMismatch: Career(#70198754219580) expected, got ActionController::Parameters(#70198701106200) in the update method
def update
#loan_plan.update(loan_plan_params)
end
When I tried to update the loan_plan model with careers params, it expects the params["careers"] should be careers object of a array instead of ids of a array.
So my workround is to manually fectch the careers objects of a array and replace the sanitized params.
It seems dirty and smells bad, any better solution in Rails way? Thanks
def loan_plan_params
# params.fetch(:loan_plan, {})
cleaned_params = params.require(:loan_plan).permit(
:id,
:name,
{:careers=>:id}
)
cleaned_params["careers"] = Career.find(cleaned_params["careers"].map{|t| t["id"]})
cleaned_params
end
model
class LoanPlan < ActiveRecord::Base
has_and_belongs_to_many :careers
accepts_nested_attributes_for :careers
end

In Rails way, params should be
"loan_plan" => {
"id" => "32",
"career_ids" => ["8", "9"]
}
and the strong parameter loan_plan_params should be
def loan_plan_params
params.require(:loan_plan).permit(
:id,
:name,
:career_ids => []
)
end

Related

Ruby on rails: Updating attribute on an association

In my controller I have the following:
def sort
params[:order].each do |key,value|
Question.find(value[:id]).update_attribute(:order,value[:order])
end
render :nothing => true
end
This works perfectly to update the order column for the 'Question' item.
However i've now moved the order column to a new table 'question_sections' which is associated to Questions.
class Question < ActiveRecord::Base
has_many :sections, :through => :question_sections
belongs_to :section
has_many :question_sections
default_scope { order(order: :asc) }
accepts_nested_attributes_for :section, :reject_if => :all_blank, allow_destroy: true
accepts_nested_attributes_for :question_sections, :reject_if => :all_blank, allow_destroy: true
end
I'm trying to adapt the sort function to update the 'order' column in 'question_sections' but am having trouble with it.
Any help on what the function should look like?
In case you are using nested attributes, you shoud call the includes method, and then iterate over each question_sections:
def sort
params[:order].each do |key,value|
questions = Question.includes(:question_sections).find(value[:id])
questions.question_sections.each { |q| q.update_attribute(:order,value[:order]) }
end
render :nothing => true
end
This breaks the problems into 2 parts, load all the question_sections needed:
1) Load all the question_sections of a question:
questions = Question.includes(:question_sections).find(value[:id])
Question Load
SELECT "questions".* FROM "questions" WHERE "questions"."id" = ? LIMIT 1 [["id", 1]]
QuestionSections Load
SELECT "question_sections".* FROM "question_sections" WHERE "question_sections"."question_id" IN (1)
2) update this question_sections
questions.question_sections.each { |q| q.update_attribute(:order,value[:order]) }
QuestionSections Update
UPDATE "question_sections" SET "order" = ?, "updated_at" = ? WHERE "question_sections"."id" = ? [["order", "different order now"], ["updated_at", "2017-03-09 13:24:42.452593"], ["id", 1]]
I think if you are using nested_attributes for Question model here then Rails should automatically update the nested params for QuestionSection
The controller should look something like this:
def sort
#question = Question.find_by(id: params[:id])
#question.update_attributes(question_params)
end
private
def question_params
params.require(:question).permit!
end
The parameters received to the controller should be like :
params = { question: {
abc: 'abc', question_sections_attributes: [
{ order_id: 1, ... },
{ order_id: 2, ... },
{ order_id: 3, ... }
]
}}
I hope this helps :)

How to combine two as_json methods to one correctly?

In my Model I have a working as_json method as follows:
def as_json(options = {})
super(options.merge(include: [:user, comments: {include: :user}]))
end
This method is for including users in comments.
Now I need to add almost the same thing in the same model for answers:
def as_json(options = {})
super(options.merge(include: [:user, answers: {include: :user}]))
end
How do I combine these two as_json methods, so that I have one as_json method?
Don't laugh but I am struggling with this for 3 days.
This is one of the reasons why you should not use the built-in to_json to serialize ActiveRecord models.
Instead, you should delegate the task to another object called serializer. Using a serializer allows you to have illimitate representations (serializations) of the same object (useful if the object can have different variants such as with/without comments, etc) and separation of concerns.
Creating your own serializer is stupid simply, as simple as having
class ModelWithCommentsSerializer
def initialize(object)
#object = object
end
def as_json
#object.as_json(include: [:user, comments: {include: :user}]))
end
end
class ModelWithAnswersSerializer
def initialize(object)
#object = object
end
def as_json
#object.as_json(include: [:user, answers: {include: :user}]))
end
end
Of course, that's just an example. You can extract the feature to avoid duplications.
There are also gems such as ActiveModelSerializers that provides that feature, however I prefer to avoid them as they tend to provide a lot of more of what most of users really need.
Why are you trying to override core Rails functionality - not good practice unless absolutely necessary.
--
This says the following:
To include associations use :include:
user.as_json(include: :posts)
# => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
# "created_at" => "2006/08/01", "awesome" => true,
# "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
# { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
You could call:
#answers.as_json(include :users)
--
Ohhhhhhhh:
Second level and higher order associations work as well:
user.as_json(include: { posts: {
include: { comments: {
only: :body } },
only: :title } })
# => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
# "created_at" => "2006/08/01", "awesome" => true,
# "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
# "title" => "Welcome to the weblog" },
# { "comments" => [ { "body" => "Don't think too hard" } ],
# "title" => "So I was thinking" } ] }
So looks like you could call:
#answers.to_json(include: comments: { include: :users })
def as_json(other_arg, options = {})
as_json(options.merge(include: [:user, other_arg: {include: :user}]))
end
And then you can call:
MyModel.as_json(:comments)
MyModel.as_json(:answers)

Accessing a hashmap as an attribute of an object

I have an instance of an object that is created like this:
Example.create(:attrib0 => {
:attrib1 => value,
:attrib2 => [
{:attrib3 => value},
{:attrib4 => value}
]
})
How can I access :attrib4?
You should use serialize in your model, then you'll be able to return the hash correctly:
class SomeModel < ActiveRecord::Base
serialize :attrib0
end
Then the following should return the hash
hash = #model.attrib0
# => {:attrib1 => value, :attrib2 => [{:attrib3 => value}, {:attrib4 => value}]
# now to access attrib4 you need to get the attrib2 array,
# then grab attrib4 by its index:
hash[:attrib2][1]
# => {:attrib4 => value}
# or to get the value:
hash[:attrib2][1][:attrib4]
# => value
The above however can get quite complex and ugly, which is why I recommended creating another model for these attributes instead.
I think you should use nested attributes. Here's how it can be:
class Example
has_one :attrib0
accepts_nested_attributes_for :attrib0
end
params = { :attrib0 => { :attrib1 => value1,
:attrib2 => [ {:attrib3 => value3}, {:attrib4 => value4} ] }
}
example = Example.create(params[:attrib0])
example.attrib0.attrib1 #=> value1
example.attrib0.attrib2 #=> [ {:attrib3 => value3}, {:attrib4 => value4} ]
Using Ruby technique only:
h = {:attrib0 => {
:attrib1 => :value1,
:attrib2 => [
{:attrib3 => :value2},
{:attrib4 => :value3}
]
}}
p h[:attrib0][:attrib2].last[:attrib4] #=> :value3

Why is the order of execution of attribute assignments failing?

I am passing these parameters to a controller:
{
"utf8" => "✓",
"authenticity_token" => "ersjaJ4/ieZelVifP/YpBHTJtiQ53HgO5KYjEdW0BlQ=",
"transaction" => {
"use_balance" => "1",
"traces_attributes" => {
"trace_ids" => ["6"],
"6" => {
"amount" => "12.0",
"charge_id" => "6"
}
},
"positive_balance" => "12",
"property_id" => "2",
"community_id" => "1"
},
"commit" => "Save Payment",
"community_id" => "1",
"property_id" => "2"
}
The controller#create then:
#payment = Transaction.new(params[:transaction])
Then the Transaction model:
belongs_to :property
belongs_to :community
attr_accessible :positive_balance
def traces_attributes=(params)
#INSIDE HERE THE VALUES OF
#params[:trace_ids] => ['6'] OK
#BUT
#self.possitive_balance => "" **NOT OK**
#self.property_id => nil **NOT OK**
end
My hypothesis is that traces_attribute= is executed before positive_balance= and property_id
Can I change this?
Why is this failing?
The order of the assignments should be the same as the order of the params in the form, but I don't think this is guaranteed.
A safer solution would be to only store the data in the traces_attributes= method, and access the other attributes later, for example in a before_save callback.
it looks based on the transaction hash that the property_id is outside of that hash so if you are building based on transactions it won't have a property_id
"transaction"=>{"use_balance"=>"1",
"traces_attributes"=>{"trace_ids"=>["6"],
"6"=>{"amount"=>"12.0",
"charge_id"=>"6"
}
},
"positive_balance"=>"12",
"property_id"=>"2",
"community_id"=>"1"
},
"commit"=>"Save Payment",
"community_id"=>"1",
"property_id"=>"2"}
do you see what i mean, the number of curly braces is messed up and prop. id isn't ending up in transactions ( i just copy and pasted your code pasted above )

Retrieve all association's attributes of an AR model?

What do you think is the most optimal way to retrieve all attributes for all the associations an AR model has?
i.e: let's say we have the model Target.
class Target < ActiveRecord::Base
has_many :countries
has_many :cities
has_many :towns
has_many :colleges
has_many :tags
accepts_nested_attributes_for :countries, :cities, ...
end
I'd like to retrieve all the association's attributes by calling a method on a Target instance:
target.associations_attributes
>> { :countries => { "1" => { :name => "United States", :code => "US", :id => 1 },
"2" => { :name => "Canada", :code => "CA", :id => 2 } },
:cities => { "1" => { :name => "New York", :region_id => 1, :id => 1 } },
:regions => { ... },
:colleges => { ... }, ....
}
Currently I make this work by iterating on each association, and then on each model of the association, But it's kind of expensive, How do you think I can optimize this?
Just a note: I realized you can't call target.countries_attributes on has_many associations with nested_attributes, one_to_one associations allow to call target.country_attributes
I'm not clear on what you mean with iterating on all associations. Are you already using reflections?
Still curious if there's a neater way, but this is what I could come up with, which more or less results in the hash you're showing in your example:
class Target < ActiveRecord::Base
has_many :tags
def associations_attributes
# Get a list of symbols of the association names in this class
association_names = self.class.reflect_on_all_associations.collect { |r| r.name }
# Fetch myself again, but include all associations
me = self.class.find self.id, :include => association_names
# Collect an array of pairs, which we can use to build the hash we want
pairs = association_names.collect do |association_name|
# Get the association object(s)
object_or_array = me.send(association_name)
# Build the single pair for this association
if object_or_array.is_a? Array
# If this is a has_many or the like, use the same array-of-pairs trick
# to build a hash of "id => attributes"
association_pairs = object_or_array.collect { |o| [o.id, o.attributes] }
[association_name, Hash[*association_pairs.flatten(1)]]
else
# has_one, belongs_to, etc.
[association_name, object_or_array.attributes]
end
end
# Build the final hash
Hash[*pairs.flatten(1)]
end
end
And here's an irb session through script/console to show how it works. First, some environment:
>> t = Target.create! :name => 'foobar'
=> #<Target id: 1, name: "foobar">
>> t.tags.create! :name => 'blueish'
=> #<Tag id: 1, name: "blueish", target_id: 1>
>> t.tags.create! :name => 'friendly'
=> #<Tag id: 2, name: "friendly", target_id: 1>
>> t.tags
=> [#<Tag id: 1, name: "blueish", target_id: 1>, #<Tag id: 2, name: "friendly", target_id: 1>]
And here's the output from the new method:
>> t.associations_attributes
=> {:tags=>{1=>{"id"=>1, "name"=>"blueish", "target_id"=>1}, 2=>{"id"=>2, "name"=>"friendly", "target_id"=>1}}}
try this with exception handling:
class Target < ActiveRecord::Base
def associations_attributes
tmp = {}
self.class.reflections.symbolize_keys.keys.each do |key|
begin
data = self.send(key) || {}
if data.is_a?(ActiveRecord::Base)
tmp[key] = data.attributes.symbolize_keys!
else
mapped_data = data.map { |item| item.attributes.symbolize_keys! }
tmp[key] = mapped_data.each_with_index.to_h.invert
end
rescue Exception => e
tmp[key] = e.message
end
end
tmp
end
end
This is updated version of Stéphan Kochen's code for Rails 4.2
def associations_attributes
association_names = self.class.reflect_on_all_associations.collect { |r| r.name }
me = self.class.includes(association_names).find self.id
pairs = association_names.collect do |association_name|
object_or_array = me.send(association_name)
if object_or_array.is_a? ActiveRecord::Associations::CollectionProxy
association_pairs = object_or_array.collect { |o| [o.id, o.attributes] }
[association_name, Hash[*association_pairs.flatten(1)]]
else
[association_name, object_or_array.attributes]
end
end
Hash[*pairs.flatten(1)]
end

Resources