Create nested attributes from JSON passed through API in Rails - ruby-on-rails

I am sending a JSON from app1 to app2 whenever a model is created in app1. I need to create a similar model in app2 along with the nested attributes. I am able to create the model but not able to figure out how to create the nested attributes model for the same in app2. How can I do that in the same controller?
models in app1
class Author
has_many :books, dependent: :destroy
accepts_nested_attributes_for :books
end
class Book
belongs_to :author
end
books_controller.rb in app1
def new
#author = Author.new
#books = #author.books.build
end
def create
#author = Author.new(author_params)
if #author.save
redirect_to author_path(#author), notice: 'Author was successfully created.'
else
render :new
end
end
def author_params
params.require(:author).permit(:name, books_attributes: [:id, :book_name, :publisher]) if params[:author]
end
api in app1
def self.create_or_update_author_in_app2(auth)
app2_author = {}
app2_author[:author_id_in_app1] = auth.id
app2_author[:name] = auth.name
app2_author[:app2_books_attributes] = auth.books.as_json(except: 'app1_author_id')
response = API.post( 'create_or_update_author', body: { request: { author_data: author_data, authenticate: {auth_key: key} } } )
end
models in app2
class App2Author
has_many :app2_books
end
class App2Book
belongs_to :app2_author
end
controller in app2
def create_or_update_author
response = params['request']['app2_author']
app2_author = App2Author.find_or_create_by(author_id_in_app1: response['author_id_in_app1'])
author.update!(name: response['name'])
app2_author.update_attributes(response['app2_books_attributes']) unless app2_author
end
At present, App2Author instances are being created in app2 but how can I create the associated books from the same json?
response received by controller in app2
Parameters: {"request"=>{"app2_author"=>{"author_id_in_app1"=>"16", "name"=>"Author 1", "app2_books_attributes"=>[{"id"=>"43", "book_name"=>"Book 1", "publisher"=>"Publisher 1", "created_at"=>"2019-07-25 15:26:57 +0530", "updated_at"=>"2019-07-25 15:26:57 +0530"},
{"id"=>"43", "book_name"=>"Book 1", "publisher"=>"Publisher 1", "created_at"=>"2019-07-25 15:26:57 +0530", "updated_at"=>"2019-07-25 15:26:57 +0530"},
{"id"=>"43", "book_name"=>"Book 1", "publisher"=>"Publisher 1", "created_at"=>"2019-07-25 15:26:57 +0530", "updated_at"=>"2019-07-25 15:26:57 +0530"},
{"id"=>"43", "book_name"=>"Book 1", "publisher"=>"Publisher 1", "created_at"=>"2019-07-25 15:26:57 +0530", "updated_at"=>"2019-07-25 15:26:57 +0530"}]}, "authenticate"=>{"auth_key"=>"my_key"}}}

The code bellow is just an idea how you can handle this.
models in app2
class App2Author
has_many :app2_books
# it's better to keep business logic in models.
def update_info(data)
name = data['name']
data['author_books'].each do |book|
books << Book.new(book)
end
save!
end
end
class App2Book
belongs_to :app2_author
end
controller in app2
def create_or_update_author
request = params['request']['author_data']
author = App2Author.find_or_create_by(author_id_in_app1: request['author_id_in_app1'])
author.update_info(request)
end
Anyway to be hones with you it's not a good approach. Rails has default mechanism to create associated objects. TO make it work in the right way you need:
1) add accepts_nested_attributes_for :app2_books to App2Author model.
class App2Author
has_many :app2_books
end
2) in the first app build valid hash with parameters and send to the second app . Something like:
app2_author: { id: 1, name: 'Author name', app2_books_attributes: [:app2_author_id, :book_name, :publisher]}
3) In the second app in controller do something like this:
author = App2Author.find_or_create_by(id: params[:id])
author.update(params) #in case the params hash is valid
that will create associations automatically.

Related

Rails JSON_API create Book with list of Genres

I'm trying to write my test to ensure creating a new book with genres assigned to it works.
I am using Active Model Serializer with the JSON_API structure (http://jsonapi.org/)
Book Model File
class Book < ApplicationRecord
belongs_to :author, class_name: "User"
has_and_belongs_to_many :genres
end
Genre Model File
class Genre < ApplicationRecord
has_and_belongs_to_many :books
end
Book Serializer file
class BookSerializer < ActiveModel::Serializer
attributes :id, :title, :adult_content, :published
belongs_to :author
has_many :genres
end
Test Sample Data
def setup
...
#fantasy = genres(:fantasy)
#newbook = {
title: "Three Little Pigs",
adult_content: false,
author_id: #jim.id,
published: false,
genres: [{title: 'Fantasy'}]
}
end
Test Method
test "book create - should create a new book" do
post books_path, params: #newbook, headers: user_authenticated_header(#jim)
assert_response :created
json = JSON.parse(response.body)
puts "json = #{json}"
assert_equal "Three Little Pigs", json['data']['attributes']['title']
genre_data = json['data']['relationships']['genres']['data']
puts "genre_data = #{genre_data.count}"
assert_equal "Fantasy", genre_data
end
Book Strong Params
def book_params
params.permit(:title, :adult_content, :published, :author_id, :genres)
end
Test Result (console response)
# Running:
......................................................json = {"data"=>{"id"=>"1018350796", "type"=>"books", "attributes"=>{"title"=>"Three Little Pigs", "adult-content"=>false, "published"=>false}, "relationships"=>{"author"=>{"data"=>{"id"=>"1027431151", "type"=>"users"}}, "genres"=>{"data"=>[]}}}}
genre_data = 0
F
Failure:
BooksControllerTest#test_book_create_-_should_create_a_new_book [/Users/warlock/App_Projects/Raven Quill/Source Code/Rails/raven-quill-api/test/controllers/books_controller_test.rb:60]:
Expected: "Fantasy"
Actual: []
bin/rails test test/controllers/books_controller_test.rb:51
Finished in 1.071044s, 51.3518 runs/s, 65.3568 assertions/s.
55 runs, 70 assertions, 1 failures, 0 errors, 0 skips
As you can see from my JSON console log, it appears my genres are not being set(need to scroll to the right in the test output above).
Please ignore this line:
assert_equal "Fantasy", genre_data
I know that's wrong. At the moment, the json is showing genre => {data: []} (empty array), that's the thing I'm trying to solve at the moment.
How do I go about creating a book with genres in this case, any ideas? :D
This is just sad...third time this week, I am answering my own question.
I finally found the answer from this Stackoverflow question:
HABTM association with Strong Parameters is not saving user in Rails 4
Turns out my strong parameters need to be:
def book_params
params.permit(:title, :adult_content, :published, :author_id, {:genre_ids => []})
end
Then my test data can be:
#fantasy = genres(:fantasy)
#newbook = {
title: "Three Little Pigs",
adult_content: false,
author_id: #jim.id,
published: false,
genre_ids: [#fantasy.id]
}
Update my test method to:
test "book create - should create a new book" do
post books_path, params: #newbook, headers: user_authenticated_header(#jim)
assert_response :created
json = JSON.parse(response.body)
assert_equal "Three Little Pigs", json['data']['attributes']['title']
genre = json['data']['relationships']['genres']['data'].first['title']
assert_equal "Fantasy", genre
end
Now my test passes.

accepts_nested_attributes_for creating duplicates

accepts_nested_attributes_for creating duplicates
Model
class Article < ActiveRecord::Base
has_many :article_collections
accepts_nested_attributes_for :article_collections, :allow_destroy => true, reject_if: :all_blank
end
class ArticleCollection < ActiveRecord::Base
belongs_to :article
end
Controller
def update
#article = Article.find_by_id(params[:id])
#article.update_attributes(params[:article])
redirect_to :index
end
Params
params = {"utf8"=>"✓"
"article"=>
{
"data_id"=>"dfe9e32c-3e7c-4b33-96b6-53b123d70e7a", "name"=>"Mass", "description"=>"mass",
"status"=>"active", "volume"=>"dfg", "issue"=>"srf", "iscode"=>"sdg",
"image"=>{"title"=>"", "caption"=>"", "root_image_id"=>""},
"article_collections_attributes"=>
[
{"name"=>"abcd", "type"=>"Special", "description"=>"content ","ordering_type"=>""}
]
},
"commit"=>"Save", "id"=>"b8c8ad67-9b98-4705-8b01-8c3f00e55919"}
Console
Article.find("b8c8ad67-9b98-4705-8b01-8c3f00e55919").article_collections.count
=> 2
Problem is whenever we are updating article it's creating multiple article_collections.
Suppose article_collections is 2 mean if we are updating article it's creating multiple article_collections = 4, it's not updating same article_collections, it's newly creating article_collections.
Why is it creating duplicates?
Your params:
params = {"utf8"=>"✓"
"article"=>
{
"data_id"=>"dfe9e32c-3e7c-4b33-96b6-53b123d70e7a", "name"=>"Mass", "description"=>"mass",
"status"=>"active", "volume"=>"dfg", "issue"=>"srf", "iscode"=>"sdg",
"image"=>{"title"=>"", "caption"=>"", "root_image_id"=>""},
"article_collections_attributes"=>
[
{"name"=>"abcd", "type"=>"Special", "description"=>"content ","ordering_type"=>""}
]
},
"commit"=>"Save", "id"=>"b8c8ad67-9b98-4705-8b01-8c3f00e55919"}
You should send and permit the "id" attribute inside the "article_collections_attributes" params. For Example,
"article_collections_attributes"=>
[
{"id"=>"2", "name"=>"abcd", "type"=>"Special", "description"=>"content ","ordering_type"=>""}
]
I think this code will help you.
Read about nested attribute and build.
In your edit action build object when it has no article_collection.
For ex.#article.article_collection.build if #article.article_collection.blank?. it will not build a new object if it has already a article collection.

Rails 3 : create two dimensional hash and add values from a loop

I have two models :
class Project < ActiveRecord::Base
has_many :ticket
attr_accessible ....
end
class Ticket < ActiveRecord::Base
belongs_to :project
attr_accessible done_date, description, ....
end
In my ProjectsController I would like to create a two dimensional hash to get in one variable for one project all tickets that are done (with done_date as key and description as value).
For example i would like a hash like this :
What i'm looking for :
#tickets_of_project = ["done_date_1" => ["a", "b", "c"], "done_date_2" => ["d", "e"]]
And what i'm currently trying (in ProjectsController) ...
def show
# Get current project
#project = Project.find(params[:id])
# Get all dones tickets for a project, order by done_date
#tickets = Ticket.where(:project_id => params[:id]).where("done_date IS NOT NULL").order(:done_date)
# Create a new hash
#tickets_of_project = Hash.new {}
# Make a loop on all tickets, and want to complete my hash
#tickets.each do |ticket|
# TO DO
#HOW TO PUT ticket.value IN "tickets_of_project" WITH KEY = ticket.done_date ??**
end
end
I don't know if i'm in a right way or not (maybe use .map instead of make a where query), but how can I complete and put values in hash by checking index if already exist or not ?
Thanx :)
I needed to do the same task before, following solution isn't pretty but should do it:
Ticket.where(:project_id => params[:id]).where("done_date IS NOT NULL").group_by {|t| t.done_date}.map {|k,v| [k => v.map {|vv| vv.value}] }.flatten.first

Build has_many :through for new Object

I might be doing this wrong overall, but maybe someone will be able to chirp in and help out.
Problem:
I want to be able to build a relationship on an unsaved Object, such that this would work:
v = Video.new
v.title = "New Video"
v.actors.build(:name => "Jonny Depp")
v.save!
To add to this, these will be generated through a custom method, which I'm attempting to modify to work, that does the following:
v = Video.new
v.title = "Interesting cast..."
v.actors_list = "Jonny Depp, Clint Eastwood, Rick Moranis"
v.save
This method looks like this in video.rb
def actors_list=value
#Clear for existing videos
self.actors.clear
value.split(',').each do |actorname|
if existing = Actor.find_by_name(actorname.strip)
self.actors << existing
else
self.actors.build(:name => actorname.strip)
end
end
end
What I expect
v.actors.map(&:name)
=> ["Jonny Depp", "Clint Eastwood", "Rick Moranis"]
Unfortunatey, these tactics neither create an Actor nor the association. Oh yeah, you might ask me for that:
in video.rb
has_many :actor_on_videos
has_many :actors, :through => :actor_on_videos
accepts_nested_attributes_for :actors
I've also tried modifying the actors_list= method as such:
def actors_list=value
#Clear for existing videos
self.actors.clear
value.split(',').each do |actorname|
if existing = Actor.find_by_name(actorname.strip)
self.actors << existing
else
self.actors << Actor.create!(:name => actorname.strip)
end
end
end
And it creates the Actor, but I'd rather not create the Actor if the Video fails on saving.
So am I approaching this wrong? Or have I missed something obvious?
Try this:
class Video < ActiveRecord::Base
has_many :actor_on_videos
has_many :actors, :through => :actor_on_videos
attr_accessor :actors_list
after_save :save_actors
def actors_list=names
(names.presence || "").split(",").uniq.map(&:strip).tap do |new_list|
#actors_list = new_list if actors_list_changes?(new_list)
end
end
def actors_list_changes?(new_list)
new_record? or
(actors.count(:conditions => {:name => new_list}) != new_list.size)
end
# save the actors in after save.
def save_actors
return true if actors_list.blank?
# create the required names
self.actors = actors_list.map {|name| Actor.find_or_create_by_name(name)}
true
end
end
You cannot assign a child to an object with no id. You will need to store the actors in your new video object and save those records after the video has been saved and has an id.

Changing type of ActiveRecord Class in Rails with Single Table Inheritance

I have two types of classes:
BaseUser < ActiveRecord::Base
and
User < BaseUser
which acts_as_authentic using Authlogic's authentication system. This inheritance is implemented using Single Table Inheritance
If a new user registers, I register him as a User. However, if I already have a BaseUser with the same email, I'd like to change that BaseUser to a User in the database without simply copying all the data over to the User from the BaseUser and creating a new User (i.e. with a new id). Is this possible? Thanks.
Steve's answer works but since the instance is of class BaseUser when save is called, validations and callbacks defined in User will not run. You'll probably want to convert the instance using the becomes method:
user = BaseUser.where(email: "user#example.com").first_or_initialize
user = user.becomes(User) # convert to instance from BaseUser to User
user.type = "User"
user.save!
You can just set the type field to 'User' and save the record. The in-memory object will still show as a BaseUser but the next time you reload the in-memory object will be a User
>> b=BaseUser.new
>> b.class # = BaseUser
# Set the Type. In-Memory object is still a BaseUser
>> b.type='User'
>> b.class # = BaseUser
>> b.save
# Retrieve the records through both models (Each has the same class)
>> User.find(1).class # = User
>> BaseUser.find(1).class # User
Based on the other answers, I expected this to work in Rails 4.1:
def update
#company = Company.find(params[:id])
# This separate step is required to change Single Table Inheritance types
new_type = params[:company][:type]
if new_type != #company.type && Company::COMPANY_TYPES.include?(new_type)
#company.becomes!(new_type.constantize)
#company.type = new_type
#company.save!
end
#company.update(company_params)
respond_with(#company)
end
It did not, as the type change would not persist. Instead, I went with this less elegant approach, which works correctly:
def update
#company = Company.find(params[:id])
# This separate step is required to change Single Table Inheritance types
new_type = params[:company][:type]
if new_type != #company.type && Company::COMPANY_TYPES.include?(new_type)
#company.update_column :type, new_type
end
#company.update(company_params)
respond_with(#company)
end
And here are the controller tests I used to confirm the solution:
describe 'Single Table Inheritance (STI)' do
class String
def articleize
%w(a e i o u).include?(self[0].to_s.downcase) ? "an #{self}" : "a #{self}"
end
end
Company::COMPANY_TYPES.each do |sti_type|
it "a newly assigned Company of type #{sti_type} " \
"should be #{sti_type.articleize}" do
post :create, { company: attributes_for(:company, type: sti_type) },
valid_session
expect(assigns(:company)).to be_a(sti_type.constantize)
end
end
Company::COMPANY_TYPES.each_index do |i|
sti_type, next_sti_type = Company::COMPANY_TYPES[i - 1],
Company::COMPANY_TYPES[i]
it "#{sti_type.articleize} changed to type #{next_sti_type} " \
"should be #{next_sti_type.articleize}" do
company = Company.create! attributes_for(:company, type: sti_type)
put :update, { id: company.to_param, company: { type: next_sti_type } },
valid_session
reloaded_company = Company.find(company.to_param)
expect(reloaded_company).to be_a(next_sti_type.constantize)
end
end
end

Resources