How to customize simple_form for json nested hash - ruby-on-rails

I have some nested data:
#preset = Preset.new
#fields is a postgres json data type
#preset.fields = {'primary_category' => {'category_id' => 57882}}
I would like to have the same nested structure preserved in the POST params[:preset][:fields] from a form submit so I have this in my form partial:
<%= text_field_tag("preset[fields][primary_category][category_id]",nil) -%>
Simple form does not know how to deal with postgres new types like hstore or json types. In my case I don't really need it for validation or to detect the data type. Is there a way I can extend SimpleForm to skip detection of the column types and just output the same existing bootstrap boilerplate that it outputs for textfields, but for my arbitrary json nested keys?
Maybe a usage like this:
<%= f.input 'preset[fields][primary_category][category_id]', :as => :json_text_field %>
To output the same thing as helper above, but surrounded with the label, and control-group classed divs, etc.
I have looked into extending the input base class per the documentation.
class JsonTextFieldInput < SimpleForm::Inputs::Base
def input
"#{#builder.text_field(???, input_html_options)}".html_safe
end
end
But here is where I get lost as I'm not sure what to pass to the #builder to bypass checking of attribute name with my own logic to map it hash keys. Also that only changes the form input and not the label, which needs some modification as well. In either case I wasn't able to get very far and I could use some guidance.

I'm using this for jsonb / json input:
class JsonbInput < SimpleForm::Inputs::StringInput
def input()
out = ActiveSupport::SafeBuffer.new
Hash[object.send(attribute_name).sort].each do | k, v|
out << template.content_tag(:div, class: 'group') do
template.concat #builder.label(k, object.send(attribute_name), label_html_options)
template.concat #builder.text_field(k, input_html_options)
end
end
out
end
def input_html_options
{class: 'string form-control'}
end
end
You also need to use store_accessor in your model.

Related

Adding a custom JSONB text_field tag with simple_form

I have a JSONB type options column on a model in my Rails application and I am in a situation where I need to provide JSONB nested parameters manually for every single form. That's why the official solution proposed here is not an option.
So I might want to have something like:
options[first_name], options[last_name]
for one instance, and a totally different set for another:
options[pet_dog], options[frank_sinatra]
I assume it can well be implemented using something like
= f.text_field :options[pet_dog]
But it's not working.
My solution code looks like this:
View form:
= f.simple_fields_for :options do |p|
- #options.each do |o|
= p.input o.name.to_sym, input_html: {value: o.default_value}
Controller
def new
#options = Model.options
...
end
def create
#job = current_user.jobs.build(job_params)
end
...
def job_params
params.require(:job_request).permit(
...
options: get_options)
end
def get_options
some_model.options.pluck(:name).map(&:to_sym)
end
Possibly this will help someone.
How did you implement the jsonb column? Have you added this to the model?
serialize :options, HashSerializer
store_accessor :options, :pet_dog, :pet_cat, :pet_cow
*Note that HashSerializer is a class acting as a serializer, which looks like this:
class HashSerializer
def self.dump(hash)
hash.to_json
end
def self.load(hash)
(hash || {}).with_indifferent_access
end
end
What those 2 lines on the model do is allowing you using accessor (getter and setter) for the json/jsonb keys. So you can do this:
object = Model.new
object.pet_dog = 'Dog1'
object.pet_cat = 'Cat1'
object.options = {"pet_dog"=>"Dog1", "pet_cat"=>"Cat1"}
Which in turn, allows you to use it as form fields:
f.text_field :pet_dog
f.text_field :pet_cat
Do not forget to whitelist the attributes in the strong_params method
You can read more on this comprehensive post by Nando Vieira:
http://nandovieira.com/using-postgresql-and-jsonb-with-ruby-on-rails
Also, if you want a shortcut, there is a gem for this:
https://github.com/devmynd/jsonb_accessor

HUGE model in rails? What would be the appropriate way to do it?

I'm practicing, and came up with an idea that I should create an experiment that involves building an editor that can customize a real car with all parts that a real car can have. And then allow the user to individually customize it down to the finest level of detail simply using true or false And then output it accordingly like:
<%= if #vehicle.steering_wheel_color == blue %>
show a blue steeringhweel
<% end %>
<%= if #vehicle.steering_wheel_color == red %>
show a red steeringhweel
<% end %>
<%= if #vehicle.gear_box_knob == brown_wood %>
show a brown wooden gear knob
<% end %>
Since we're talking roughly around 500-1000 parameters(?) I'm sure that my idea of Architecture is preeeetty bad, so I'm wondering what the 'correct' or even the best way of doing it would be? considering that each variable needs to be queryable?
I've done some googling and I found one answer that advised to do one model that belongs to vehicle that houses all the booleans. But then I asked a friend and he said that would be a terrible idea.
I guess my question is, what's a good way to build a huge car-editor?
I would suggest virtus and a jsonb db field as a possible starting point. If you install the virtus gem, then you could set up a pseudo-model called something like CarSettings. This would look something like:
class CarSettings
include Virtus.model
attribute :steering_wheel_color, String, default: ''
attribute :gear_box_knob, String, default: ''
attribute :has_spoiler, Boolean, default: false
...
def self.dump(settings)
settings.to_hash
end
def self.load(settings)
new(settings)
end
end
These settings then are serialized to/from json in your Car (or whatever) class like this:
class Car < ApplicationRecord
(stuff)
serialize :settings, CarSettings
end
Then you can use standard RoR AR accessors to get to the settings:
car.settings.steering_wheel_color = ...
Not sure how you are planning to get steering_wheel_color from just booleans, but maybe you want to set up enums for the options for each "thing" and use dropdowns to present instead of checkboxes to present? I've used the classy_enum gem for this, but just regular enums would work as well.
EDIT:
Forgot some things you'll need in the Car model:
class Car < ApplicationRecord
...
attr_accessor :steering_wheel_color, :gear_box_knob, etc
def method_missing(m, *args, &block)
self.settings.send(m, *args, &block)
end
def respond_to_missing?(method_name, include_private = false)
names = CarSettings.attribute_set.map { |a| a.name }
names.include? method_name
end
end
In Rails 4.2, this was working without the attr_accessors, however, for some reason in Rails 5, it gives an unknown attribute error without the attr_accessors when trying to do .update_attributes(car_params). Still trying to determine why that changed, but the attr_accessors work for now.

ActiveAdmin get value from form text field

I'm new in Ruby and make some simple admin using ActiveAdmin.
I have a model Question which I want to create, fill and store to database, it has an attribute themes (array of Theme models). When user creates a new record he doesn't enter themes manually but provide some string and system will automatically parse it and find or create themes. So I have code like that:
form do |f|
f.inputs "Questions Details" do
f.input :question, as: :string
f.input :autocomplete_themes, hint: "You should enter here multiple themes,
divide them with `,` or `;`"
end
f.actions
end
It creates a new field autocomplete_themes for entering string and it doesnt' exist in model Question. So what I want - is to get autocomplete_themes value like string and then use split() and my custom logic - but it gives an error.
before_create do |question|
array = []
puts "******"
puts :autocomplete_themes.text
themeTitles = :autocomplete_themes.split(",") #split(/,|;/)
for title in themeTitles do
theme = Theme.find_by(title: title)
theme = Theme.create(title: title) unless theme
array << theme
end
question.themes = array
end
Question: how can I get autocomplete_themes value as string? Thx!
Update: as I understood here - it looks like the similiar case, but there was problem with setting default value to custom field, but I need to get its value from code.
You did not specify the error you were getting, but based on the information you gave, you don't need autocomplete_themes to be a real database-backed attribute of the Question model, but rather you only need the information temporarily so that your before_create filter can use it to execute the appropriate logic.
Therefore, you could make autocomplete_themes a "virtual attribute" that is like a traditional member variable of an instance of Question.
class Question < ActiveRecord::Base
attr_writter :autocomplete_themes
attr_reader :autocomplete_themes
...other code
end
This will allow you to do things like:
#question.autocomplete_themes = "1,2,3"
themes_text = #question.auto_complete_themes
And best of all, ActiveAdmin supports having form inputs assign to virtual attributes. So you can keep your form like this:
form do |f|
f.inputs "Questions Details" do
f.input :question, as: :string
f.input :autocomplete_themes, hint: "You should enter here multiple themes,
divide them with `,` or `;`"
end
f.actions
end
And your before_filter would look like this:
before_create do |question|
array = []
themeTitles = question.autocomplete_themes.split(",") #split(/,|;/)
for title in themeTitles do
theme = Theme.find_by(title: title)
theme = Theme.create(title: title) unless theme
array << theme
end
question.themes = array
end

How does rails parse POST request parameters?

I am working on an application where I send a post request to my rails server with parameters stored in JSON format. Let's say my application routes the request to the create function on my cats_controller. The cat model has three fields :name, :hunger, :mood. Now each cat has_many kittens. My Kitten model has two fields :cat_id (referring to which cat owns it, because my kitten belongs_to a cat) and :cute. Now every time i create a cat on my application I want to do it with a single call, so I call /cats.json from my app with a POST request. The parameters are stored in JSON format and they include the following fields {name:"",hunger:"", mood:"",cute:""}. Now the controller takes these params creates a new cat, and then creates a new kitten assigned to this cat using cat.kittens.build(). The kitten then just needs to use the last parameter I sent "mood:" to get created properly.
Now the question is this, when I print the params variable from this controller I get the following hash: {name:"", hunger:"", mood:"", cute:"", cat:{name:"", hunger:"", mood:""}}. Why does this happen?
How does Rails parse the POST request params and take me from
{name:"",hunger:"", mood:"",cute:""}
to
{name:"", hunger:"", mood:"", cute:"", cat:{name:"", hunger:"", mood:""}}
How is this "cat" hash generated, when, and what rules does it follow?
Then my followup question would be, since rails 4 forces you to whitelist parameters before you use them. I am doing:
params.require(:cat).permit(:name,:hunger,:mood)
How do I also permit the :cute value?
You'll be best reading up on HTTP - Difference between GET and POST.
It's not Rails which sends the request, it's plain old HTML.
The difference you're looking at is how your server picks up the POST params, considering GET params are passed through the url...
Note that query strings (name/value pairs) is sent in the URL of a GET
request:
Note that query strings (name/value pairs) is sent in the HTTP message
body of a POST request
Thus, the base level answer to your question is that you need to append your POST params to the message body of the request. The best example I know of how to do this is with JQuery ajax:
$.ajax({
url: ...,
data: {your: key, value: pairs}
});
To answer your other questions, here's the structure you should use:
#app/models/cat.rb
class Cat < ActiveRecord::Base
#columns id | name | mood | created_at | updated_at
has_and_belongs_to_many :kittens
class_name: "Cat",
join_table: :kittens,
foreign_key: :cat_id,
association_foreign_key: :kitten_id
alias_attribute :born, :created_at #-> allows you to call #cat.born
end
#kittens table
#columns cat_id | kitten_id
You can read up about the self join here: Rails: self join scheme with has_and_belongs_to_many?
This should give you the ability to create a cat, and have him/her assigned as a kitten of another cat:
#app/controllers/cats_controller.rb
class CatsController < ApplicationController
def new
#cat = Cat.new
end
def create
#cat = Cat.new cat_params
#cat.save
end
private
def cat_params
params.require(:cat).permit(:name, :mood, :kittens)
end
end
This will give you the ability to have the following:
#app/views/cats/new.html.erb
<%= form_for #cat do |f| %>
<%= f.text_field :name %>
<%= f.text_field :mood %>
<%= f.collection_select :kittens, Cat.all, :id, :name %>
<%= f.submit %>
<% end %>
This will also allow you to call:
#cat = Cat.find params[:id]
#cat.kittens.each do |kitten|
kitten.mood #-> "cute"
This is down to something called parameter wrapping
It's there as a convenience so that you don't have to submit the root (ie in your case put everything in the user element) but still get to use params[:user]
By default if you have parameter wrapping on for the request format then CatsController will do this for any parameter that matches Cat.attribute_names. You can customise how parameter wrapping is done for a controller (or turn it off, on control which content types trigger it) with the wrap_parameters method, for example
class CatsController < ActionController::Base
wrap_parameters :cat, include: [:cute]
end
To also include the cute in the list of parameters to wrap and you can then do
params.require(:cat).permit(:name, :hunger, :mood, :cute)
#Frederick Cheung: thank you so much for the link. Like #Curse, I have been trying to figure out why I get two copies (ie: duplicates) of my JSON HTTP POST params printed in the rails log. Adding
wrap_parameters format: []
at the top of my controller definition gave me
{name:"",hunger:"", mood:"",cute:""}
in my log output instead of
{name:"", hunger:"", mood:"", cute:"", cat:{name:"", hunger:"", mood:""}}
because I realized I had
wrap_parameters format: [:json]
in config/initializers/wrap_parameters.rb. Having wrap_parameters enabled can almost double log size if large JSON documents are being POSTed to a Rails 3 server...

Rails 4 Dynamic generate Hstore keys and automatically add it into whitelist

I have a cgi_attributes hstore type column in the UrlCommand model.
class UrlCommand < ActiveRecord::Base
store_accessor :cgi_attributes, :name, :range, :security, :default_value
end
However, the keys in cgi_attributes should be dynamically added by users.
And I also want to render each key as a input filed in my form
Rather than hard-code the columns
- [:name, :range, :security].each do | column |
= render :partial => 'attributes' , :locals => { f: f, column: column }
And also need to add those dynamically generated key can be update into my Model.
def url_command_params
params.require(:url_command).permit(:model_name, :firmware_version, :cgi_attributes,
:cgi_name,:name, :range, :security)
end
Now, All of my code are based on hard-code, How to make the keys and value can be dynamically added by users and store into the UrlCommand model ?
Maybe you can create a separate table for the attribute names (CustomAttributes) and then create a loop that adds them with store_accessor like this:
CustomAttributes.all.each do |attribute|
store_accessor :cgi_attributes attribute.name
end
I guess you can do something similar to strong parameters. I have not tried this my self, because I'm struggeling to get hstore working in my application. But i will have the same user situation/ problem to solve. So i will test it and change my answer once i get it working and confirmed.
a bit like it is explained in this railscast:
http://railscasts.com/episodes/403-dynamic-forms

Resources