What should the strong parameters for my chapters_controller be if I have a Book entity and a Chapter entity?
Note: I am using JSON API.
In my chapters_controller, should my strong parameters be:
:title, :order, :content, :published, :book, :picture
Or should it be:
:title, :order, :content, :published, :book_id, :picture
If I use :book instead of :book_id, then in my Ember application, when I go to create a new chapter, I am able to create it and associate this chapter to the parent book, however, my test fails:
def setup
#book = books(:one)
#new_chapter = {
title: "Cooked Wolf Dinner",
order: 4,
published: false,
content: "The bad wolf was very mad. He was determined to eat the little pig so he climbed down the chimney.",
book: #book
}
end
def format_jsonapi(params)
params = {
data: {
type: "books",
attributes: params
}
}
return params
end
...
test "chapter create - should create new chapter assigned to an existing book" do
assert_difference "Chapter.count", +1 do
post chapters_path, params: format_jsonapi(#new_chapter), headers: user_authenticated_header(#jim)
assert_response :created
json = JSON.parse(response.body)
attributes = json['data']['attributes']
assert_equal "Cooked Wolf Dinner", attributes['title']
assert_equal 4, attributes['order']
assert_equal false, attributes['published']
assert_equal #book.title, attributes['book']['title']
end
end
I get error in my console saying Association type mismatch.
Perhaps my line:
book: #book
is causing it?
Either way, gut feeling is telling me I should be using :book in my chapters_controller strong parameters.
It's just my test isn't passing, and I am not sure how to write the parameter hash for my test to pass.
After a few more hours of struggle and looking at the JSON API docs:
http://jsonapi.org/format/#crud-creating
It has come to my attention, in order to set a belongsTo relationship to an entity with JSON API, we need do this:
POST /photos HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "photos",
"attributes": {
"title": "Ember Hamster",
"src": "http://example.com/images/productivity.png"
},
"relationships": {
"photographer": {
"data": { "type": "people", "id": "9" }
}
}
}
}
This also led me to fixing another problem I had in the past which I couldn't fix. Books can be created with multiple genres.
The JSON API structure for assigning an array of Genre to a Book entity, we replace the data hash with a data array in the relationship part like this:
"data": [
{ "type": "comments", "id": "5" },
{ "type": "comments", "id": "12" }
]
Additonally, in my controllers, anything strong parameters like so:
:title, :content, genre_ids: []
Becomes
:title, :content, :genres
To comply with JSON API.
So for my new test sample datas I now have:
def setup
...
#new_chapter = {
title: "Cooked Wolf Dinner",
order: 4,
published: false,
content: "The bad wolf was very mad. He was determined to eat the little pig so he climbed down the chimney.",
}
...
end
def format_jsonapi(params, book_id = nil)
params = {
data: {
type: "chapters",
attributes: params
}
}
if book_id != nil
params[:data][:relationships] = {
book: {
data: {
type: "books",
id: book_id
}
}
}
end
return params
end
Special note on the relationship settings - only add relationships to params if there is a relationship, otherwise, setting it to nil is telling JSON API to remove that relationship, instead of ignoring it.
Then I can call my test like so:
test "chapter create - should create new chapter assigned to an existing book" do
assert_difference "Chapter.count", +1 do
post chapters_path, params: format_jsonapi(#new_chapter, #book.id), headers: user_authenticated_header(#jim)
assert_response :created
json = JSON.parse(response.body)
attributes = json['data']['attributes']
assert_equal "Cooked Wolf Dinner", attributes['title']
assert_equal 4, attributes['order']
assert_equal false, attributes['published']
assert_equal #book.id, json['data']['relationships']['book']['data']['id'].to_i
end
Related
I have order model which has one serialise column as order_details. When I tries to call index action it returns the hash in order_details key.
I want all the values of order_details in main object of order.
My order model as below:
# models/order.rb
class Order < ActiveRecord::Base
belongs_to :user
serialize :order_details
....
end
Controller
# controllers/orders_controller.rb
class OrdersController < ApplicationController
def index
orders = Order.select('id, user_id, total, order_details')
render json: orders, status: :ok
end
end
The JSON response I received is as below:
[{
"order":{
"id":1,
"user_id":1,
"total": 1000,
"order_details":{
"payment_done_by":"credit/debit",
"transaction_id":"QWERTY12345",
"discount":210,
"shipping_address": "This is my sample address"
}
},
{
"order":{
"id":2,
"user_id":2,
"total": 500,
"order_details":{
"payment_done_by":"net banking",
"transaction_id":"12345QWERTY",
"discount":100,
"shipping_address": "This is my sample address 2"
}
}
]
But here I need response in below format
[{
"order":{
"id":1,
"user_id":1,
"total": 1000,
"payment_done_by":"credit/debit",
"transaction_id":"QWERTY12345",
"discount":210,
"shipping_address": "This is my sample address"
},
{
"order":{
"id":2,
"user_id":2,
"total": 500,
"payment_done_by":"net banking",
"transaction_id":"12345QWERTY",
"discount":100,
"shipping_address": "This is my sample address 2"
}
]
I was trying to parsed each response using each but result can have hundreds of user object.
Please help here.
Thanks in advance.
Krishna
you should add as_json to your Order model to override the existing method with the same name for you to meet your expected output
def as_json(options = nil)
if options.blank? || options&.dig(:custom)
attrs = attributes.slice("id", "user_id", "total")
attrs.merge(order_details)
else
super
end
end
then, in your controller
def index
orders = Order.all
render json: orders, status: :ok
end
hope that helps
FYR: https://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json
In my Rails (api only) learning project, I have 2 models, Group and Artist, that have a many-to-many relationship with a joining model, Role, that has additional information about the relationship. I have been able to save m2m relationships before by saving the joining model by itself, but here I am trying to save the relationship as a nested relationship. I'm using the jsonapi-serializer gem, but not married to it nor am I tied to the JSON api spec. Getting this to work is more important than following best practice.
With this setup, I'm getting a 500 error when trying to save with the following errors:
Unpermitted parameters: :artists, :albums and ActiveModel::UnknownAttributeError (unknown attribute 'relationships' for Group.)
I'm suspecting that my problem lies in the strong param and/or the json payload.
Models
class Group < ApplicationRecord
has_many :roles
has_many :artists, through: :roles
accepts_nested_attributes_for :artists, :roles
end
class Artist < ApplicationRecord
has_many :groups, through: :roles
end
class Role < ApplicationRecord
belongs_to :artist
belongs_to :group
end
Controller#create
def create
group = Group.new(group_params)
if group.save
render json: GroupSerializer.new(group).serializable_hash
else
render json: { error: group.errors.messages }, status: 422
end
end
Controller#group_params
def group_params
params.require(:data)
.permit(attributes: [:name, :notes],
relationships: [:artists])
end
Serializers
class GroupSerializer
include JSONAPI::Serializer
attributes :name, :notes
has_many :artists
has_many :roles
end
class ArtistSerializer
include JSONAPI::Serializer
attributes :first_name, :last_name, :notes
end
class RoleSerializer
include JSONAPI::Serializer
attributes :artist_id, :group_id, :instruments
end
Example JSON payload
{
"data": {
"attributes": {
"name": "Pink Floyd",
"notes": "",
},
"relationships": {
"artists": [{ type: "artist", "id": 3445 }, { type: "artist", "id": 3447 }]
}
}
Additional Info
It might help to know that I was able to save another model with the following combination of json and strong params.
# Example JSON
"data": {
"attributes": {
"title": "Wish You Were Here",
"release_date": "1975-09-15",
"release_date_accuracy": 1
"notes": "",
"group_id": 3455
}
}
# in albums_controller.rb
def album_params
params.require(:data).require(:attributes)
.permit(:title, :group_id, :release_date, :release_date_accuracy, :notes)
end
From looking at https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html I think the data format that Rails is normally going to expect will look something like:
{
"group": {
"name": "Pink Floyd",
"notes": "",
"roles_attributes": [
{ "artist_id": 3445 },
{ "artist_id": 3447 }
]
}
}
with a permit statement that looks something like (note the . before permit has moved):
params.require(:group).
permit(:name, :notes, roles_attributes: [:artist_id])
I think you have a few options here:
Change the data format coming into the action.
Craft a permit statement that works with your current data (not sure how tricky that is), you can test your current version in the console with:
params = ActionController::Parameters.new({
"data": {
"attributes": {
"name": "Pink Floyd",
"notes": "",
},
"relationships": {
"artists": [{ type: "artist", "id": 3445 }, { type: "artist", "id": 3447 }]
}
}
})
group_params = params.require(:data).
permit(attributes: [:name, :notes],
relationships: [:artists])
group_params.to_h.inspect
and then restructure the data to a form the model will accept; or
Restructure the data before you try to permit it e.g. something like:
def group_params
params_hash = params.to_unsafe_h
new_params_hash = {
"group": params_hash["data"]["attributes"].merge({
"roles_attributes": params_hash["data"]["relationships"]["artists"].
map { |a| { "artist_id": a["id"] } }
})
}
new_params = ActionController::Parameters.new(new_params_hash)
new_params.require(:group).
permit(:name, :notes, roles_attributes: [:artist_id])
end
But ... I'm sort of hopeful that I'm totally wrong and someone else will come along with a better solution to this stuff.
With my current project, I'm receiving a large JSON file that I'm parsing and storing into my database. The problem is I feel like I'm structuring my database in a very inefficient way.
Example of JSON:
{
first_name: "John",
records: {
ids: [110, 725, 2250],
count: [1, 1, 6]
},
items: {
top: {
title: "My top",
values: { value: [51, 50, 70] }
},
middle: {
title: "Middle Stuff",
values: { value: [51] }
},
},
values: {
health: 100,
strength: 250,
mana: 50
}
}
As you can see the JSON is fairly complex, with nested Objects.
While building it, I started with the main Object ( user ), then slowly started adding more objects. Values was easy, so I added that as another table and just with a reference to the user_id.
Then I did records, which is a bit more complex, but works. However, I'm very worried about the most nested parts, that could be 5+ objects deep. I feel like I shouldn't have an entire column row for a simple value.
What would be the best way to improve on this? Should I somehow crunch the data and store it differently?
Thanks for your help.
// JSON response
{
"name": "John Smith",
...
"progression": {
"levels": [{
"name": "Level 1",
"bosses": [
{
"name": "Boss 1",
"difficultyCompleted": "Hard"
},
{
"name": "Boss 2",
"difficultyCompleted": "Hard"
}]
},{
"name": "Level 2",
"bosses": [
{
"name": "Boss 3",
"difficultyCompleted": "Normal"
},
{
"name": "Boss 4",
"difficultyCompleted": "Easy"
}]
}
}
}
In this example JSON, there is a few layers for each boss that the user has completed. What I would have thought to do initially was to create models and tables but that seemed like it would be wasteful not only in memeory, but also would take longer to fetch the current progression for Boss 4.
Example:
User has_one Progression.
Progression has_one Levels.
Levels has_many Dungeons.
Dungeons has_many bosses.
What I did instead was trying to compress the bosses into a single field, and just convert the JSON at runtime.
So, instead my structure would be like this.
User has_one Progression.
Progression has_many Dungeons.
Models:
// Progression.rb
class Progression < ApplicationRecord
has_many :dungeons
def self.initialize(params={})
params = params['levels']
prog = Progression.new()
params.each do |dungeon|
prog.dungeons.append( Dungeon.initialize( dungeon ) )
end
return prog
end
end
// Dungeon.rb
class Dungeon < ApplicationRecord
belongs_to :progression
def self.initialize(params={})
Dungeon.new(params.reject { |k| !Dungeon.attribute_method?(k) }) # Used to ignore any unused parameters that don't exist on the model.
end
# To convert `bosses` from JSON into a hash for easy use.
def get_bosses
JSON.parse bosses.gsub( '=>', ':' )
end
end
Migration:
// xxxxxx_create_progression.rb
class CreateProgression < ActiveRecord::Migration[5.0]
def change
create_table :progressions do |t|
t.integer :character_id
end
create_table :dungeons do |t|
t.integer :character_id
t.integer :progression_id
t.string :name
t.text :bosses
end
add_index :progressions, :character_id
add_index :dungeons, :progression_id
end
end
Now, when a User is updated to fetch their progression, I can set their progression.
// users_controller.rb
def update_progression
progression = ... fetched from the response
#user.progression = Progression.initialize(progression)
end
After that's all saved to the user, you can now fetch the progression back by going:
<% user.progression.dungeons.each do |dungeon| %>
<%= dungeon.name %>
<% end %>
This solution seems like a decent mix, but I'm a bit worried about the parsing of the JSON. It could become too much, but I'll have to keep watching it. Any other ideas or improvements would be greatly appreicated.
I'm trying to write an update method that processes JSON. The JSON looks like this:
{
"organization": {
"id": 1,
"nodes": [
{
"id": 1,
"title": "Hello",
"description": "My description."
},
{
"id": 101,
"title": "fdhgh",
"description": "My description."
}
]
}
}
Organization model:
has_many :nodes
accepts_nested_attributes_for :nodes, reject_if: :new_record?
Organization serializer:
attributes :id
has_many :nodes
Node serializer:
attributes :id, :title, :description
Update method in the organizations controller:
def update
organization = Organization.find(params[:id])
if organization.update_attributes(nodes_attributes: node_params.except(:id))
render json: organization, status: :ok
else
render json: organization, status: :failed
end
end
private
def node_params
params.require(:organization).permit(nodes: [:id, :title, :description])
end
I also tried adding accepts_nested_attributes_for to the organization serializer, but that does not seem to be correct as it generated an error (undefined method 'accepts_nested_attributes_for'), so I've only added accepts_nested_attributes_for to the model and not to the serializer.
The code above generates the error below, referring to the update_attributes line in the update method. What am I doing wrong?
no implicit conversion of String into Integer
In debugger node_params returns:
Unpermitted parameters: id
{"nodes"=>[{"id"=>101, "title"=>"gsdgdsfgsdg.", "description"=>"dgdsfgd."}, {"id"=>1, "title"=>"ertret.", "description"=>"etewtete."}]}
Update: Got it to work using the following:
def update
organization = Organization.find(params[:id])
if organization.update_attributes(nodes_params)
render json: organization, status: :ok
else
render json: organization, status: :failed
end
end
private
def node_params
params.require(:organization).permit(:id, nodes_attributes: [:id, :title, :description])
end
To the serializer I added root: :nodes_attributes.
It now all works, but I'm concerned about including the id in node_params. Is that safe? Wouldn't it now be possible to edit the id of the organization and node (which shouldn't be allowed)? Would the following be a proper solution to not allowing it to update the id's:
if organization.update_attributes(nodes_params.except(:id, nodes_attributes: [:id]))
looks super close.
Your json child object 'nodes' need to be 'nodes_attributes'.
{
"organization": {
"id": 1,
"nodes_attributes": [
{
"id": 1,
"title": "Hello",
"description": "My description."
},
{
"id": 101,
"title": "fdhgh",
"description": "My description."
}
]
}
}
You can do this sort of thing. Put this in your controller.
before_action do
if params[:organization]
params[:organization][:nodes_attributes] ||= params[:organization].delete :nodes
end
end
It will set the correct attribute in params and still use all the accepts_nested_attributes features.
I'm trying to POST some data to my Rails 4 API.
The resource:
App.factory 'House', ['$resource', ($resource) ->
$resource '/api/v1/houses/:id', { id: '#id' }
]
The JSON representation of the resource:
newHouse = {
"owner_id": "30",
"name": "test",
"address_attributes": {
"street": "somewhere",
"city": "on",
"region": "earth"
}
}
On House.save(null, $scope.newHouse), the server logs give me this:
Processing by Api::V1::HouseController#create as JSON
Parameters: {"owner_id"=>"34", "name"=>"test", "address_attributes"=>{"street"=>"somewhere", "city"=>"on", "region"=>"earth"}, "house"=>{"name"=>"test", "owner_id"=>"34"}}
Which is underisable, to say the least.
owner_id and name appear directly bellow root, and below "house" - I would expect only below "house"
address_attributes appears only directly bellow root - I would expect it to be below house
Basically I want this:
Processing by Api::V1::HouseController#create as JSON
Parameters: {"house"=>{"name"=>"test", "owner_id"=>"34", "address_attributes"=>{"street"=>"somewhere", "city"=>"on", "region"=>"earth"}}}
Any help?
EDIT
Rails controller action:
def create
house = House.new house_params
if house.save
head 200
else
render json: {
"error" => "validation errors",
"detail" => house.errors.messages
}, status: 422
end
end
def house_params
fields = [:name, :owner_id, address_attributes: [:street, :city, :region, :country, :postal_code ]]
params.require(:house).permit(fields)
end
The model House has:
has_one :address
accepts_nested_attributes_for :address
I don't want to change the way the server handles the data. Many backends expect parameters to be held in the format I want, and even jQuery does it in its AJAX calls. AngularJS should be the one to change, or at least allow ways to configure.
I am not sure you are using your $resource correctly.
EDIT: I am not sure why your class method behaves like this, the requests made should be equal./EDIT
One example of an alternative way to do it would be this:
Lets say your newHouse model is this:
{
"owner_id": "30",
"name": "test",
"address_attributes": {
"street": "somewhere",
"city": "on",
"region": "earth"
}
}
In your html you would write something like:
<span class="btn btn-danger" ng-click="createNewHouse(newHouse)">Create new house</span>
Your Controller would bind createNewHouse() method to $scope:
$scope.createNewHouse= function(newHouse){
var newHouseInstance = new House();
newHouseInstance.house = newHouse;
newHouseInstance.$save();
}
Above code gave me a POST request with this payload (direct copy from Chrome developer console):
Remote Address:127.0.0.1:8080
Request URL:http://localhost:8080/api/v1/houses
Request Method:POST
Request Payload:
{"house":{"owner_id":"30", "name":"test", "address_attributes":{"street":"somewhere", "city":"on", "region":"earth"}}}
This will nicely translate to what you need back at the server.
Good luck!