Rails JSON_API create Book with list of Genres - ruby-on-rails

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.

Related

Create nested attributes from JSON passed through API in 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.

Rails 4 Integration Testing Arrays not present in json object inside controllers

I'm trying to create an integration testing for the creating of a record called books. I'm having problems creating the hash in the tests. This is my code:
test/integration/creating_book_test.rb
require 'test_helper'
class CreatingBookTest < ActionDispatch::IntegrationTest
def setup
#michael_lewis = Author.create!(name: 'Michael Lewis')
#business = Genre.create!(name: 'Business')
#sports = Genre.create!(name: 'Sports')
#analytics = Genre.create!(name: 'Analytics')
end
test "book is created successfully" do
post '/api/books', { book: book_attributes }.to_json, {
'Accept' => 'application/json',
'Content-Type' => 'application/json'
}
... assertions...
end
def book_attributes
{title: 'Moneyball',
year: 2003,
review: 'Lorem Ipsum',
rating: 5,
amazon_id: '10832u13kjag',
author_ids: [#michael_lewis.id],
genre_ids: [#business.id, #sports.id, #analytics.id]
}
end
end
In the controller, I'm whitelisting the params with:
def book_params
params.require(:book).permit(:title, :year, :review, :rating, :amazon_id, :author_ids, :genre_ids)
end
The problem is that I'm not receiving :author_ids and :genre_ids in the controller. It seems like arrays don't get sent to the controller, so I can't test that associations work like they should.
Thanks.
You strong paramter declaration is wrong. Here is the fix:
params.require(:book).permit(:title, :year, :review, :rating, :amazon_id, author_ids: [], genre_ids: [])
From Permitted Scalar Values documentation :
..To declare that the value in params must be an array of permitted scalar values map the key to an empty array.

Rails nested resources and devise

Let's say I have a devise model called "User" which has_many :notes and :notebooks and each :notebook has_many :notes.
So a notes will have two foreign key, :user_id and :notebook_id, so how to build/find a note?
current_user.notebooks.find(param).notes.new(params[:item]) will create the foreign_key only for notebook or also for the user in the note record in the DB?
If the second case (foreign key only for notebook), how should I write?
Using MongoDB with MongoID and referenced relations
Mongoid will manage the document references and queries for you, just make sure to specify the association/relationship for each direction that you need (e.g., User has_many :notes AND Note belongs_to :user). Like ActiveRecord, it appears to be "smart" about the relations. Please do not manipulate the references manually, but instead let your ODM (Mongoid) work for you. As you run your tests (or use the rails console), you can tail -f log/test.log (or log/development.log) to see what MongoDB operations are being done by Mongoid for you and you can see the actual object references as the documents are updated. You can see how a relationship makes use of a particular object reference, and if you pay attention to this, the link optimization should become clearer.
The following models and test work for me. Details on the setup are available on request. Hope that this helps.
Models
class User
include Mongoid::Document
field :name
has_many :notebooks
has_many :notes
end
class Note
include Mongoid::Document
field :text
belongs_to :user
belongs_to :notebook
end
class Notebook
include Mongoid::Document
belongs_to :user
has_many :notes
end
Test
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
User.delete_all
Note.delete_all
Notebook.delete_all
end
test "user" do
user = User.create!(name: 'Charles Dickens')
note = Note.create!(text: 'It was the best of times')
notebook = Notebook.create!(title: 'Revolutionary France')
user.notes << note
assert_equal(1, user.notes.count)
user.notebooks << notebook
assert_equal(1, user.notebooks.count)
notebook.notes << note
assert_equal(1, notebook.notes.count)
puts "user notes: " + user.notes.inspect
puts "user notebooks: " + user.notebooks.inspect
puts "user notebooks notes: " + user.notebooks.collect{|notebook|notebook.notes}.inspect
puts "note user: " + note.user.inspect
puts "note notebook: " + note.notebook.inspect
puts "notebook user: " + notebook.user.inspect
end
end
Result
Run options: --name=test_user
# Running tests:
user notes: [#<Note _id: 4fa430937f11ba65ce000002, _type: nil, text: "It was the best of times", user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), notebook_id: BSON::ObjectId('4fa430937f11ba65ce000003')>]
user notebooks: [#<Notebook _id: 4fa430937f11ba65ce000003, _type: nil, user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), title: "Revolutionary France">]
user notebooks notes: [[#<Note _id: 4fa430937f11ba65ce000002, _type: nil, text: "It was the best of times", user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), notebook_id: BSON::ObjectId('4fa430937f11ba65ce000003')>]]
note user: #<User _id: 4fa430937f11ba65ce000001, _type: nil, name: "Charles Dickens">
note notebook: #<Notebook _id: 4fa430937f11ba65ce000003, _type: nil, user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), title: "Revolutionary France">
notebook user: #<User _id: 4fa430937f11ba65ce000001, _type: nil, name: "Charles Dickens">
.
Finished tests in 0.018622s, 53.6999 tests/s, 161.0998 assertions/s.
1 tests, 3 assertions, 0 failures, 0 errors, 0 skips
I would use
class User
has_many :notebooks
has_many :notes, :through => :notebooks
end
http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association
Update
You could always set the user_id manually, like this (I assume param is the ID for your notebook?):
Notebook.find(param).notes.new(params[:item].merge(:user_id => current_user.id))

Mongoid query referencing an ID but not using an embedded document

I have a Mongo collection that simply references an ID to another collection. Hypothetically the collection I'm specifically referring to might be called:
Walks. Walks has a reference to owner_id. The owner takes many walks with his many pets every day. What I want to do is query Walks for a list of N owner_ids and get only the last walk they took for each owner and group that by owner_id. To get a list of all walks by said list we'd do something like.
Walk.any_in(:owner_id => list_of_ids)
My question is, is there a way to query that list_of_ids, get only one walk per owner_id (the last one they took which can be sorted by the field created_at and returned in a hash where each walk is pointed to by an owner_id such as:
{ 5 => {..walk data..}, 10 => {.. walk data ..}}
Here's an answer that uses MongoDB's group command.
For the purposes of testing, I've used walk_time instead of created_at.
Hope that this helps and that you like it.
class Owner
include Mongoid::Document
field :name, type: String
has_many :walks
end
class Walk
include Mongoid::Document
field :pet_name, type: String
field :walk_time, type: Time
belongs_to :owner
end
test/unit/walk_test.rb
require 'test_helper'
class WalkTest < ActiveSupport::TestCase
def setup
Owner.delete_all
Walk.delete_all
end
test "group in Ruby" do
walks_input = {
'George' => [ ['Fido', 2.days.ago], ['Fifi', 1.day.ago], ['Fozzy', 3.days.ago] ],
'Helen' => [ ['Gerty', 4.days.ago], ['Gilly', 2.days.ago], ['Garfield', 3.days.ago] ],
'Ivan' => [ ['Happy', 2.days.ago], ['Harry', 6.days.ago], ['Hipster', 4.days.ago] ]
}
owners = walks_input.map do |owner_name, pet_walks|
owner = Owner.create(name: owner_name)
pet_walks.each do |pet_name, time|
owner.walks << Walk.create(pet_name: pet_name, walk_time: time)
end
owner
end
assert_equal(3, Owner.count)
assert_equal(9, Walk.count)
condition = { owner_id: { '$in' => owners[0..1].map(&:id) } } # don't use all owners for testing
reduce = <<-EOS
function(doc, out) {
if (out.last_walk == undefined || out.last_walk.walk_time < doc.walk_time)
out.last_walk = doc;
}
EOS
last_walk_via_group = Walk.collection.group(key: :owner_id, cond: condition, initial: {}, reduce: reduce)
p last_walk_via_group.collect{|r|[Owner.find(r['owner_id']).name, r['last_walk']['pet_name']]}
last_walk = last_walk_via_group.collect{|r|Walk.new(r['last_walk'])}
p last_walk
end
end
test output
Run options: --name=test_group_in_Ruby
# Running tests:
[["George", "Fifi"], ["Helen", "Gilly"]]
[#<Walk _id: 4fbfa7a97f11ba53b3000003, _type: nil, pet_name: "Fifi", walk_time: 2012-05-24 15:39:21 UTC, owner_id: BSON::ObjectId('4fbfa7a97f11ba53b3000001')>, #<Walk _id: 4fbfa7a97f11ba53b3000007, _type: nil, pet_name: "Gilly", walk_time: 2012-05-23 15:39:21 UTC, owner_id: BSON::ObjectId('4fbfa7a97f11ba53b3000005')>]
.
Finished tests in 0.051868s, 19.2797 tests/s, 38.5594 assertions/s.
1 tests, 2 assertions, 0 failures, 0 errors, 0 skips

Problems with validates_inclusion_of, acts_as_tree and rspec

I have problems to get rspec running properly to test validates_inclusion_of my migration looks like this:
class CreateCategories < ActiveRecord::Migration
def self.up
create_table :categories do |t|
t.string :name
t.integer :parent_id
t.timestamps
end
end
def self.down
drop_table :categories
end
end
my model looks like this:
class Category < ActiveRecord::Base
acts_as_tree
validates_presence_of :name
validates_uniqueness_of :name
validates_inclusion_of :parent_id, :in => Category.all.map(&:id), :unless => Proc.new { |c| c.parent_id.blank? }
end
my factories:
Factory.define :category do |c|
c.name "Category One"
end
Factory.define :category_2, :class => Category do |c|
c.name "Category Two"
end
my model spec looks like this:
require 'spec_helper'
describe Category do
before(:each) do
#valid_attributes = {
:name => "Category"
}
end
it "should create a new instance given valid attributes" do
Category.create!(#valid_attributes)
end
it "should have a name and it shouldn't be empty" do
c = Category.new :name => nil
c.should be_invalid
c.name = ""
c.should be_invalid
end
it "should not create a duplicate names" do
Category.create!(#valid_attributes)
Category.new(#valid_attributes).should be_invalid
end
it "should not save with invalid parent" do
parent = Factory(:category)
child = Category.new #valid_attributes
child.parent_id = parent.id + 100
child.should be_invalid
end
it "should save with valid parent" do
child = Factory.build(:category_2)
child.parent = Factory(:category)
# FIXME: make it pass, it works on cosole, but I don't know why the test is failing
child.should be_valid
end
end
I get the following error:
'Category should save with valid
parent' FAILED Expected #<Category id:
nil, name: "Category Two", parent_id:
5, created_at: nil, updated_at: nil>
to be valid, but it was not Errors:
Parent is missing
On console everything seems to be fine and work as expected:
c1 = Category.new :name => "Parent Category"
c1.valid? #=> true
c1.save #=> true
c1.id #=> 1
c2 = Category.new :name => "Child Category"
c2.valid? #=> true
c2.parent_id = 100
c2.valid? #=> false
c2.parent_id = 1
c2.valid? #=> true
I'm running rails 2.3.5, rspec 1.3.0 and rspec-rails 1.3.2
Anybody, any idea?
The problem is that you can't put a call to Category.all.map(&:id) inside the called to validates_inclusion_of.
The first indication that this is the case will be apparent when you try to run
rake db:migrate:down VERSION=<n>
rake db:migrate:up VERSOIN=<n>
where <n> is the version number of the migration that creates the Category model.
You will get something like:
in /Users/sseefried/tmp/so)
== CreateCategories: reverting ===============================================
-- drop_table(:categories)
-> 0.0032s
== CreateCategories: reverted (0.0034s) ======================================
(in /Users/sseefried/tmp/so)
rake aborted!
SQLite3::SQLException: no such table: categories: SELECT * FROM "categories"
(See full trace by running task with --trace)
This is because rake tries to load app/models/category.rb before running the migration. Because the Category model does not exist it fails.
Another way to see the problem is to do tail -f log/development.log and then try to open a console with script/console. You will see an SQL query of the form:
SELECT * FROM "categories"
in the output. This corresponds to the call to Category.all.map(:&id). However, once you start typing commands like:
c1 = Category.new, :name => "Category 1"
you will see that the query SELECT * from "categories" does not reappear in the log. The moral of the story is only constants can appear in calls to validations_inclusion_of because the code in there will only be evaluated once..
The only reason your console code worked was because, in a previous console session, you had created a Category object with id=1
You can write a custom validation that does what you want with:
validate :parent_exists
protected
def parent_exists
ids = Category.all.map(&:id)
if !parent_id.blank? && !ids.member?(parent_id)
errors.add(:parent_id, "does not point to a valid parent record")
end
end
Your rspec tests will pass once you have added this code.
Actually, you can defer enumerable calculation by simply putting Category.all.map(&:id) into a proc/lambda.
Writing
validates_inclusion_of :parent_id,
in: proc{ Category.all.map(&:id) },
unless: proc{ |c| c.parent_id.blank? }
will fetch categories' ids at the time of validation, not at the time of class declaration.

Resources