How to store boolean value in jsonb column - ruby-on-rails

I'm getting a checkbox value from my form like this
<%= f.label 'Most Popular', class: "form-label" %>
<%= check_box_tag "price_template[preferences][pcb_budget_options][][most_popular]",
true,
f.object.preferences.dig("pcb_budget_options", index, "most_popular") %>
And the params I am permitting is like this
params.require(:price_template).permit(:currency_id,
:program_id,
:active,
:default,
country_ids: [],
preferences: [budget_options: [:amount, :most_popular, :text],
pcb_budget_options: [:amount, :most_popular]])
and it stored in DB like this
{
"budget_options"=>[
{"amount"=>"1.0", "text"=>"budget options"},
{"amount"=>"2.0", "most_popular"=>"true", "text"=>"budget options"},
{"amount"=>"3.0", "text"=>"budget options"}
],
"pcb_budget_options"=>[
{"amount"=>"1.0"},
{"amount"=>"0.0"},
{"amount"=>"-1.0", "most_popular"=>"true"}
]
}
but the most_popular value is stored here is in string format but I want to store it as a boolean.

It's not possible in jsonb it will be always save as a "string" in the DB.
You can create your own serializer to parse the jsonb as you want or if only the boolean part matter for you, you can create a method in your model like this
class Mymodel
def budget_most_popular
ActiveRecord::Type::Boolean.new.cast(preferences["budget_options"]["most_popular"])
end
def pcb_budget_most_popular
ActiveRecord::Type::Boolean.new.cast(preferences["pcb_budget_options"]["most_popular"])
end
end
So anywhere in your code you can get a real boolean value by calling this method.
Imagine you have the following record
$> MyModel.first
<MyModel:0x029I23EZ
id: 1,
name: "foo",
preferences: {
"budget_options" =>
[
{ "amount"=>"10", "most_popular"=>"true", "text"=>"" },
{"amount"=>"0.0", "text"=>""}, {"amount"=>"0.0", "text"=>""}
],
"pcb_budget_options"=>
[
{"amount"=>"20", "most_popular"=>"false"}, {"amount"=>"0.0"},
{"amount"=>"0.0"}
]
}
...
And somewhere in your code you need to check if budget_option is most_popular
class MyModelController
def show
#mymodel = MyModel.first
if #mymodel.budget_most_popular
render "template/most_popular"
else
render "template/less_popular"
end
end
end
Since in our last record budget_options as most_popular set as 'true' our model method budget_most_popular will cast this 'true'to return a boolean true so our show will render the most_popular template
Here is an interesting blog article about the jsonb with Rails

Related

Cleanest way to create an object from JSON?

I have a class (not active record) and I would like to create objects from API data.
Since fields name/structure don't match, I don't think that it's possible to use params as we would use with forms.
That's why I'm mapping the attributes as follow:
job = Job.new()
job.id = attributes['id']
job.title = attributes['fields']['title']
job.body = attributes['fields']['body-html']
job.how_to_apply = attributes['fields']['how_to_apply-html'].presence
attributes['fields']['city'].each { |city| job.cities << city['name'] } if attributes['fields']['city']
attributes['fields']['country'].each { |country| job.countries << country['name'] }
job.start_date = Date.parse(attributes['fields']['date']['created'])
job.end_date = Date.parse(attributes['fields']['date']['closing'])
attributes['fields']['source'].each { |source| job.sources << source['name'] }
attributes['fields']['categories'].each { |category| job.categories << category['name'] }
job
attributes is the data part of a JSON response.
What do you guys think?
A more readable way is to have an initializer in Job and call it like this:
job = Job.new(
id: attributes['id'],
title: attributes['fields']['title'],
body: attributes['fields']['body-html'],
how_to_apply: attributes['fields']['how_to_apply-html'].presence,
cities: attributes['fields']['city']&.map { |city| city['name'] },
countries: attributes['fields']['country'].map { |country| country['name'] },
start_date: Date.parse(attributes['fields']['date']['created']),
end_date: Date.parse(attributes['fields']['date']['closing']),
sources: attributes['fields']['source'].map { |source| source['name'] },
categories: attributes['fields']['categories'].map { |category| category['name'] }
)
initializer can take named parameters or just a options hash (not recommended):
class Job < ...
def initializer(id:, title:, cities: nil, and_so_on__:)
self.id = id
# ...
end
end
You can use .tap method, its a little bit cleaner this way. Also some things can be moved to methods, for example:
fields = attributes['fields']
job = Job.new.tap do |j|
j.id = attributes['id']
j.title = fields['title']
j.body = fields['body-html']
j.how_to_apply = fields['how_to_apply-html'].presence
j.start_date = date_parser(fields['date']['created'])
j.end_date = date_parser(fields['date']['closing'])
j.countries = fields['country'].map { |country| country['name'] }
j.cities = fields['city']&.map { |city| city['name'] }
(...)
end
def date_parser(date)
Date.parse(date)
end
Since this question is tagged Rails you can use ActiveModel::Model and ActiveModel::Attributes to create a rich model with typecasting, validations etc.
Then just create a factory method to create model instances from raw JSON:
class Job
include ActiveModel::Model
include ActiveModel::Attributes
attribute :id, :integer
attribute :title, :string
attribute :body, :string
attribute :how_to_apply, :string
attribute :start_date, :date
attribute :end_date, :date
# Unfortunately ActiveModel::Attributes does not support array attributes
attr_accessor :city
attr_accessor :country
attr_accessor :source
attr_accessor :categories
def self.from_json(**attributes)
# use attributes.fetch('fields') instead if you
# want to raise and halt execution
fields = attributes['fields']
new(attributes.slice('id', 'title')) do |job|
job.assign_attributes(
body: fields['body-html'],
how_to_apply: fields['how_to_apply-html'],
city: fields['city']&.map {|c| c['name'] },
country: fields['country']&.map {|c| c['name'] },
start_date: fields.dig('date', 'created'),
end_date: fields.dig('date', 'closing'),
source: fields['source']&.map {|s| s['name'] },
categories: fields['categories']&.map {|c| c['name'] }
) if fields
end
end
end
If this method glows to an unruly size or if the complexity increases you can use the adapter pattern or a serializer.
Since fields name/structure don't match, I don't think that it's possible to use params as we would use with forms.
This is not quite true. ActionController::Parameters is really just a Hash like object and you can use .merge to manipulate it just like a hash:
params = ActionController::Parameters.new(json_hash)
.permit(:id, :title, fields: {})
params .slice(:id, :title).merge(
how_to_apply: params[:fields]['how_to_apply-html'],
# ...
)

Update has_many attributes with irregular params

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

Unpermitted parameter for array with dynamic keys

I'm trying to permit an array with an arbitrary number of values, but Rails throws Unpermitted parameter: service_rates every time. I tried a lot of things (Rails 4 Unpermitted Parameters for Array, Unpermitted parameters for Dynamic Forms in Rails 4, ...) but nothing works.
The field's name is service_rates and it's column type is jsonb.
I want to create a JSON object from an arbitrary number of input fields:
<%= f.hidden_field :service_ids, value: #services.map(&:id) %>
<% #services.each do |service| %>
<tr>
<td>
<% value = #project.service_rates ? #project.service_rates["#{service.id}"]['value'] : '' %>
<%= text_field_tag "project[service_rates][#{service.id}]", value, class: 'uk-width-1-1', placeholder: 'Stundensatz' %>
</td>
</tr>
<% end %>
So my POST data looks like this:
project[service_rates][1] = 100
project[service_rates][2] = 95
project[service_rates][3] = 75
Currently service_rates is permitted via whitelisting with tap:
def project_params
params.require(:project).permit(:field1, :field2, […], :service_ids).tap do |whitelisted|
whitelisted[:service_rates] = params[:project][:service_rates]
end
end
At least, I'm building a JSON object in a private model function (which throws this error):
class Project < ActiveRecord::Base
before_save :assign_accounting_content
attr_accessor :service_ids
private
def assign_accounting_content
if self.rate_type == 'per_service'
service_rates = {}
self.service_ids.split(' ').each do |id|
service_rates["#{id}"] = {
'value': self.service_rates["#{id}"]
}
end
self.service_rates = service_rates
end
end
end
I've also tried to permit the field like that …
params.require(:project).permit(:field1, :field2, […], :service_rates => [])
… and that …
params.require(:project).permit(:field1, :field2, […], { :service_rates => [] })
… but this doesn't work either.
When I try this …
params.require(:project).permit(:field1, :field2, […], { :service_rates => [:id] })
… I get this: Unpermitted parameters: 1, 3, 2
It's not really clear what service_rates is for you. Is it the name of an association ? Or just an array of strings ?
To allow array of strings : :array => [],
To allow nested params for association : association_attributes: [:id, :_destroy, ...]
params.require(:object).permit(
:something,
:something_else,
....
# For an array (of strings) : like this (AFTER every other "normal" fields)
:service_rates => [],
# For nested params : After standard fields + array fields
service_rates_attributes: [
:id,
...
]
)
As I explained in the comments, the order matters. Your whitelisted array must appear AFTER every classic fields
EDIT
Your form should use f.fields_for for nested attributes
<%= form_for #project do |f| %>
<%= f.fields_for :service_rates do |sr| %>
<tr>
<td>
<%= sr.text_field(:value, class: 'uk-width-1-1', placeholder: 'Stundensatz' %>
</td>
</tr>
<% end %>
<% end %>

Rails jbuilder: how to build a JSON from an array with the key equals to the array values

I'm a pretty new Rails developer, I'm using Jbuilder to build my view in the following way:
[:aaa, :bbb, :ccc].each do |value|
json.value do |json| #<------ Here is my error!
json.partial! foo.send(value)
end
end
Everything works BUT the json.value, my response is the following (obviously):
[{
"value" => {...}
"value" => {...}
"value" => {...}
}]
I'd like to have this one instead:
[{
"aaa" => {...}
"bbb" => {...}
"ccc" => {...}
}]
Any ideas?
From the guide:
To define attribute and structure names dynamically, use the set!
method:
json.set! :author do
json.set! :name, 'David'
end
# => "author": { "name": "David" }
The solution is so:
[:aaa, :bbb, :ccc].each do |value|
json.set! value do |json|
json.partial! foo.send(value)
end
end

Rabl showing an empty json block

I am trying Rabl, however, I seem to receive a practically empty json block.
require_dependency "api/application_controller"
module Api
class RentablePropertiesController < ApplicationController
respond_to :json
def index
#r = Core::RentableProperty.all
# render :text => #r.to_json --> note: this renders the json correctly
render "api/rentable_properties/index" #note: rabl here does not
end
end
end
index.json.rabl
collection #r
Output
[{"rentable_property":{}}]
Note: with a simply #r.to_json, it renders correctly:
[{"id":1,"description":"description","property_type_id":1,"created_at":"2013-08-22T19:04:35.000Z","updated_at":"2013-08-22T19:04:35.000Z","title":"Some Title","rooms":null,"amount":2000.0,"tenure":null}]
Any idea why rabl doesn't work?
The documentation of RABL (https://github.com/nesquena/rabl#overview) says that you need to precise what attributes you want to show in your JSON.
Their example:
# app/views/posts/index.rabl
collection #posts
attributes :id, :title, :subject
child(:user) { attributes :full_name }
node(:read) { |post| post.read_by?(#user) }
Which would output the following JSON or XML when visiting /posts.json:
[{ "post" :
{
"id" : 5, title: "...", subject: "...",
"user" : { full_name : "..." },
"read" : true
}
}]

Resources