Constructing a nested JSON request with Jbuilder - ruby-on-rails

Currently my JSON request is returning the below, where each person/lender has many inventories.
#output of /test.json
[
{"id":13, "email":"johndoe#example.com", "inventories":
[
{"id":10,"name":"2-Person Tent","category":"Camping"},
{"id":11,"name":"Sleeping bag","category":"Camping"},
{"id":27,"name":"6-Person Tent","category":"Camping"}
]
},
{"id":14, "email":"janedoe#example.com", "inventories":
[
{"id":30,"name":"Electric drill","category":"Tools"},
{"id":1,"name":"Hammer","category":"Tools"},
{"id":37,"name":"Plane","category":"Tools"}
]
}
]
I need to nest in one more thing and am having trouble doing so. For context, each inventory item is referenced via it's id as a foreign key in a borrow record. Each borrow record belongs to a request parent that stores returndate and pickupdate. What I need now, is for each inventory item, to nest an array of all the request records, with information on pickupdate and returndate. In other words, desired output:
[
{"id":13, "email":"johndoe#example.com", "inventories":
[
{"id":10,"name":"2-Person Tent","category":"Camping", "requests":
[
{"id":1, "pickupdate":"2014-07-07","returndate":"2014-07-10"},
{"id":2, "pickupdate":"2014-06-02","returndate":"2014-06-05"},
{"id":3, "pickupdate":"2014-08-14","returndate":"2014-08-20"}
]
},
{"id":11,"name":"Sleeping bag","category":"Camping", "requests":
[
{"id":4, "pickupdate":"2014-05-27","returndate":"2014-05-30"},
{"id":5, "pickupdate":"2014-04-22","returndate":"2014-04-25"}
]
},
{"id":27,"name":"6-Person Tent","category":"Camping", "requests":
[
{"id":6, "pickupdate":"2014-07-10","returndate":"2014-07-12"}
]
}
]
},
{"id":14, "email":"janedoe#example.com", "inventories":
...
I have written the following code:
json.array!(#lenders) do |json, lender|
json.(lender, :id, :email)
json.inventories lender.inventories do |json, inventory|
json.id inventory.id
json.name Itemlist.find_by_id(inventory.itemlist_id).name
#code below says, json.requests should equal all the Requests where there is a Borrows within that Request that is using the Inventory in question
json.requests Request.select { |r| r.borrows.select { |b| b.inventory_id == inventory.id }.present? } do |json, request|
json.pickupdate request.pickupdate
json.returndate request.returndate
end
end
end
When I refresh the page, I get wrong number of arguments (0 for 2..5)
I feel like the issue is that the Request.select... is returning an Array which isn't what needs to go here... but in the earlier nested function lender.inventories is an Inventory::ActiveRecord_Associations_CollectionProxy though I'm not sure how to correct for this.
NOTE: Someone said the problem could be that unlike with the nesting between inventories and lender, there's not an explicit association between inventory and request, but then again the line json.name Itemlist.find_by_id(inventory.itemlist_id).name worked so I'm not sure this is right. (Also if this is the case, I'm not sure how to bypass this limitation... I currently don't want to create a relationship between the two.)
Thanks!

ARG. Ok so this code is perfectly right. The issue was that I"m using the Gon gem in conjunction with Jbuilder, and Request is a predefined class in Gon.
So just changed code to
#requestrecords.select....
And in the controller:
#requestrecords = Request.all
-__-

Related

Rails Strong Parameters - Allow parameter to be an Array or String

Using Strong Parameters in my Rails Controller, how can I state that a permitted parameter can be a String or an Array?
My strong params:
class SiteSearchController < ApplicationController
[...abbreviated for brevity...]
private
def search_params
params.fetch(:search, {}).permit(:strings)
end
end
I'm wanting to POST string(s) to search as a String or as an Array:
To search for 1 thing:
{
"strings": "search for this"
}
OR, to search for multiple things:
{
"strings": [
"search for this",
"and for this",
"and search for this string too"
]
}
Update:
Purpose: I'm creating an API where my users can "batch" requests (getting responses via web-hooks), or make a one-off request (getting an immediate response) all on the same endpoint. This question is only 1 small part of my requirement.
The next piece will be doing this same logic, where I'll allow the search to happen on multiple pages, i.e.:
[
{
"page": "/section/look-at-this-page",
"strings": "search for this"
},
{
"page": "/this-page",
"strings": [
"search for this",
"and for this",
"and search for this string too"
]
}
]
OR on a single page:
{
"page": "/section/look-at-this-page",
"strings": "search for this"
}
(which will make me need to have Strong Params allow an Object or an Array to be sent.
This seems like a basic thing, but I'm not seeing anything out there.
I know that I could just make the strings param an array, and then require searching for 1 thing to just have 1 value in the array... but I want to have this parameter be more robust than that.
You can just permit the parameter twice - once for an array and once for a scalar value.
def search_params
params.fetch(:search, {}).permit(:strings, strings: [])
end
You could check if params[:strings] is an array and work from there
def strong_params
if params[:string].is_a? Array
params.fetch(:search, {}).permit(strings: [])
else
params.fetch(:search, {}).permit(:strings)
end
end

Strong Parameters - Can't Access the Deeply-Nested Attributes

One StackOverflow question has asked what I need, but the self-answer didn't help me know what to do next. The scenario presented in that question (whitelisting deeply nested strong parameters in rails) is pretty much what I've got going on, but I'll post an abbreviation of mine (still very long) and hope someone--maybe even the Dave of the post--can help (I don't have enough reputation to comment and ask him). There are few links about nested strong parameters I haven't read, and I've dealt with some on many controllers and API endpoints, but this is the most complex in the app. (I'm including such a long example so you can see the full complexity.)
This is on the sales_controller, and one of the attributes we can't get to is the timezone_name, which is in the run_spans_attributes, which is in the options_attributes under sale. I've tried just about all of the different syntax approaches that match most of the nested attributes with strong parameters issues here on StackOverflow, but none of it has worked. Do I need more classes? Are there magic brackets? I need new suggestions. Please.
It should be noted that this is with the strong_parameters gem and Rails 3.2.21, but I want to get the app ready for Rails 4, so I'm hoping to avoid a short-term solution.
Sorry it's so long:
Parameters:
"sale"=>{
"cloned_from"=>"",
"type"=>"Localsale",
"primary_contact_attributes"=>{
"primary"=>"true",
"first_name"=>"Fred",
"id"=>"1712"
},
"contract_signed_on"=>"March 20, 2015",
"billing_addresses_attributes"=>{
"0"=>{
"billing"=>"1",
"city"=>"San Diego",
"id"=>"29076"
}
},
"other_contacts_attributes"=>{
"0"=>{
"first_name"=>"Fred",
"_destroy"=>"false",
"id"=>"170914"
},
"1"=>{
"first_name"=>"Fred",
"last_name"=>"Smith",
"_destroy"=>"false",
"id"=>"1798"
}
},
"opportunity_detail_attributes"=>{
"original_salesperson_id"=>"",
"id"=>"10130"
},
"production_editor"=>"1868097",
"event_sale_attributes"=>{
"0"=>{
"name"=>"is_super_sale",
"value"=>"0",
"id"=>"15326"
},
"1"=>{
"name"=>"super_show_code",
"value"=>""
},
},
"scheduling_note"=>"",
"category_ids"=>["2", "364"],
"options_attributes"=>{
"0"=>{
"title"=>"T-Shirt and Bag Check",
"event_starts_at(1i)"=>"2015",
"event_starts_at(2i)"=>"6",
"event_doors_open_at_attributes"=>{
"option_id"=>"8682604",
"doors_time(1i)"=>"",
"id"=>"278382"
},
"event_option_attributes"=>{
"0"=>{
"name"=>"event_duration",
"value"=>""
},
"1"=>{
"name"=>"send_pre_event_email",
"value"=>"1",
"id"=>"632546"
}
},
"language_id"=>"1",
"run_spans_attributes"=>{
"0"=>{
"timezone_name"=>"Eastern Time (US & Canada)",
"_destroy"=>"false",
"id"=>"560878"
},
"1429320288130"=>{
"timezone_name"=>"Eastern Time (US & Canada)",
"_destroy"=>"false"
}
},
"_destroy"=>"false",
"id"=>"8682604"
}#ends 0 option
},#ends options
"coupons_per_redemption"=>"1",
"methods_attributes"=>{
"0"=>{
"redemption_code"=>"0",
"_destroy"=>"0",
"id"=>"9797012"
},
"1"=>{
"redemption_code"=>"4",
"_destroy"=>"1",
"vendor_provided_promo_code"=>"0",
"promo_code"=>""
}
}, #ends redemption methods
"addresses_attributes"=>{
"0"=>{
"street_address_1"=>"2400 Cat St",
"primary"=>"0",
"id"=>"2931074",
"_destroy"=>"false"
}
},
"zoom"=>"",
"video_attributes"=>{
"youtube_id"=>"",
},
"updated_from"=>"edit"
}
Help me do this right? By the way, all kinds of .tap do |whitelisted| approaches have failed.
private
def_sale_strong_params
params.require(:sale).permit(:how, :the, :heck, :do, :the_attributes =>
[:make, themselves => [:known, :outside => [:of, :these => [:darn,
:parentheses], :and], :brackets]])
end
1. Validate Your Progress in the Console
To configure Strong Parameters, it can help to work in the Rails console. You can set your parameters into an ActiveSupport::HashWithIndifferentAccess object and then start to test out what you'll get back from ActionController::Parameters.
For example, say we start with:
params = ActiveSupport::HashWithIndifferentAccess.new(
"sale"=>{
"cloned_from"=>"",
"type"=>"Localsale"}
)
We can run this:
ActionController::Parameters.new(params).require(:sale).permit(:cloned_from, :type)
And we'll get this as the return value and we'll know we've successfully permitted cloned_from and type:
{"cloned_from"=>"", "type"=>"Localsale"}
You can keep working up until all parameters are accounted for. If you include the entire set of parameters that you had in your question...
params = ActiveSupport::HashWithIndifferentAccess.new(
"sale"=>{
"cloned_from"=>"",
"type"=>"Localsale",
...
"options_attributes"=>{
"0"=>{
"title"=>"T-Shirt and Bag Check",
...
"run_spans_attributes"=>{
"0"=>{
"timezone_name"=>"Eastern Time (US & Canada)",
...
)
...you can get down to timezone_name with a structure that will look something like this:
ActionController::Parameters.new(params).require(:sale).permit(:cloned_from, :type, options_attributes: [:title, run_spans_attributes: [:timezone_name]])
The return value in the console will be:
{"cloned_from"=>"", "type"=>"Localsale", "options_attributes"=>{"0"=>{"title"=>"T-Shirt and Bag Check", "run_spans_attributes"=>{"0"=>{"timezone_name"=>"Eastern Time (US & Canada)"}, "1429320288130"=>{"timezone_name"=>"Eastern Time (US & Canada)"}}}}}
2. Break the Work Into Smaller Parts
Trying to handle all of the allowed attributes for each model all on one line can get confusing. You can break it up into several lines to make things easier to understand. Start with this structure and fill in the additional attributes for each model:
video_attrs = [] # Fill in these empty arrays with the allowed parameters for each model
addresses_attrs = []
methods_attrs = []
run_spans_attrs = [:timezone_name]
event_option_attrs = []
options_attrs = [:title, event_option_attributes: event_option_attrs, run_spans_attributes: run_spans_attrs]
event_sale_attrs = []
opportunity_detail_attrs = []
other_contacts_attrs = []
billing_addresses_attrs = []
primary_contact_attrs = []
sales_attributes = [
:cloned_from,
:type,
primary_contact_attributes: primary_contact_attrs,
billing_addresses_attributes: billing_addresses_attrs,
other_contacts_attributes: other_contacts_attrs,
options_attributes: options_attrs,
opportunity_detail_attributes: opportunity_detail_attrs,
event_sale_attributes: event_sale_attrs,
methods_attributes: methods_attrs,
addresses_attributes: addresses_attrs,
video_attributes: video_attrs
]
Then you can just send *sales_attributes into the permitted parameters. You can verify in the console with:
ActionController::Parameters.new(params).require(:sale).permit(*sales_attributes)
Try this out:
params.require(:sale).permit(:options_attributes => [:other_attributes, { :run_spans_attributes => [:timezone_name] }]
Going by what I did in my controller, here's what I'd assume would work:
params.require(:sale).permit(:cloned_form, ... billing_addresses_attributes: [:id, :city, :building] ...)
Edfectively, put brackets around the attributes like "0"=> and don't forget to adf :id and :_destroy like I mentioned in my answer, as I ended up creating other models and used accepts_nested_attributes_for.
For posterity, I want to let folks know what we finally discovered: Well, the information was out there, and in fact, I read it in Russ Olsen's Eloquent Ruby, which I thankfully remembered during a debugging session with a more advanced developer: "With the advent of Ruby 1.9, however, hashes have become firmly ordered."
Why was that important? I'd alphabetized the attributes on the controller, and because of how the run_span_attributes were set up, they NEEDED timezone_name to be in the front. And if it wasn't, then it couldn't get to it. You can only imagine how debugging this tormented us. But at least we know now: ORDER MATTERS. So, if all else fails, remember that.
After some experimentation, I found this problem only seemed to exist with hash keys that were integer strings.
When I replaced integers strings with non-integer strings, strong parameters accepted the hash.
Assuming we can't change the data format in our params, one workaround is to allow arbitrary hashes at the level where you run into trouble (eg. :run_spans_attributes => {}).
If your form is used to modify a model, this open up the risk of mass assignment for the models you open up with the arbitrary hash -- you will want to keep that in mind.

active_model_serializers for different classes in array

FeedController returns an array with objects of these classes: Product, Kit and Article.
Is it possible and how with active_model_serializers apply ProductSerializer for Product, KitSerializer for Kit and ArticleSerializer for Article?
This should render something like this:
[
{ "type": "product", other fiels of Product },
{ "type": "kit", other fiels of Kit },
{ "type": "article", other fiels of Article }
]
This should work with version 0.9.0, and version 0.10.0 should probably support this out of the box when it is finally ready, but at the time of this answer, it was suggested that you not use that version (master/edge)
class MyArraySerializer < ActiveModel::ArraySerializer
def initialize(object, options={})
options[:each_serializer] = get_serializer_for(object)
super(object, options)
end
private
def get_serializer_for(klass)
serializer_class_name = "#{klass.name}Serializer"
serializer_class = serializer_class_name.safe_constantize
if serializer_class
serializer_class
elsif klass.superclass
get_serializer_for(klass.superclass)
end
end
end
You can modify the get_serializer_for method to better suit your needs. I used this for handling STI subclasses where my parent class had all of the attributes defined. You will however need individual serializers for each of your individual objects since the attributes will most likely vary.
Then in your controller:
render json: #my_array_of_mixed_classes, serializer: MyArraySerializer
If this is no possible with active_model_serializers out of the box, maybe the reason is that you have a somewhat non-standard design of your API response. e.g. if I had to consume your API, would it be easy for me to deserialize your JSON response?
One way to get around this issue would therefore be to redesign your API response:
...
"products" : [ ARRAY of Product ],
"kits" : [ ARRAY of Kit ],
"articles" : [ ARRAY of Article ]
....
That would be easier to deserialize by the API consumer as well.

How can I create this JSON structure with JBuilder and Rails?

TL;DR:
How can i use jbuilder to create JSON that looks like this?
[
{}, // Your new pagination state
[{}, ...] // An array of JSON objects
]
Longer version:
I am trying get pagination working with Backbone.js and backbone-pageable https://github.com/wyuenho/backbone-pageable .
Backbone-pageable requires that the JSON returned be formatted such that it's an array of two objects. The first object is a regular object containing pagination control information. The second object should be an array of your actual data formatted as Backbone would normally expect. These expectations are hard coded into backbone-pageable's parse methods (source)
It seems weird to have an array of un-like items but for this use case it seems acceptable.
For json i've used rabl in the past but for science I'm trying to use jbuilder and I've gotten this far...
JSON structure (wrong):
{
"current_page": 1,
"total_pages": 6,
...,
"entries": [
{ "id": 131 },
...
]
}
Using this code:
json.current_page #posts.current_page
...
json.entries #posts do |post|
json.extract! post, :id...
end
Closer, but still very wrong :/
Thank you
Can you try this:
json.array! [0,1] do |index|
if index == 0
json.current_page #posts.current_page
...
else
json.entries #posts do |post|
json.extract! post, :id...
end
end
end

Ruby on Rails: reverse lookup of array list of values

i have a model with a user selectable option that is set up in an array on the model.
def Pie < ActiveRecored::Base
def self.sel_options
[ [ "Apple Blueberry", "AB" ],
[ "Cranberry Date", "CD" ] ]
end
end
while the short string is retrieved from elsewhere and stored in the database, i would like to display the longer string when showing the object. e.g. in the view use:
Pie.display_customeor_choice[#pie_flavor]
i don't want to hard code the reverse hash, but if i create a display_options method that converts the array to a hash with reverse mapping will it run the conversion every time display_options is called? this could be resource intensive with large arrays that are converted a lot, is there a way to create the reverse hash once when the app is started and never again? (using rails 3 and ruby 1.9.2)
You are looking for Array#rassoc
Pie.display_customeor_choice.rassoc("#pie_flavor")
Here's how you could do it:
def Pie < ActiveRecored::Base
def self.sel_options
[ [ "Apple Blueberry", "AB" ],
[ "Cranberry Date", "CD" ] ]
end
def self.display_customeor_choice
unless #options
#options = {}
sel_options.each { |items| #options[items.last] = items.first }
end
#options
end
end
This guarantees it's going to be loaded only once on production (or other environments where cache_classes is set to true) but always reloads on development mode, making it simpler for you to change and see the changes.

Resources