Generate form based on fields defined in database - ruby-on-rails

I would like to generate rails form runtime based on the fields defined in database.
Current Structure of database
**fields table**
- id
- field_name
- field_value
- field_type (text field, date, dropdown)
- field_values (Basically for list of values to be shown for dropdown field_type)
- field_validation
Eg: If my database fields table stores following data
1 | employee_name | text_field | | {blank => false, :length => 3..50}
2 | gender | dropdown | male, female | {blank => false}
Now based on above data I want to generate form as shown below
<%= simple_form_for(#field, url: eval(url)) do |f| %>
<%= f.input_text :employee_name %>
<%= f.collection :gender %>
<% end %>

I'm using ActiveModel for solving pretty much the same problem.
# somewhere in metainfo model
def class_from_meta
klass = Class.new do
include ActiveModel::Model
def self.name
'Person'
end
end
[:id, :email, :age].each do |attr|
klass.instance_eval do
attr_accessor attr
validates_presence_of attr
end
end
klass
end
# in controller
def create
#person = MetaInfo.find(params[:meta_info_id]).class_from_meta.new
respond_with #person
end
With class_eval and instance_eval you can generate a class which will behave prettly much like ActiveRecord

Related

Rails fields_for 10 nested optional models, display some filled and some blank on edit

I have a model A that can have up to 10 associated models B in a one-to-many relationship. These nested models have just a string attribute representing a word.
I want to display a form to create/edit the parent model and all the nested children, displaying fields for the 10 possible models. Then, if I only fill up two of them, two models will be created.
Finally, when editing model A I need to display 10 fields, two of them filled up with the model B associated with A data, and the rest blank ready to fill.
Tried fields_forwith an array, but it only displays fields for the already existing model B instances.
View:
= form_for #a, remote: true do |f|
= f.text_field :title, placeholder: true
= f.fields_for :bs, #a.bs do |ff|
/ Here, for the edit action, N text fields appear, being N equals to #soup.soup_words.size
/ and I need to display 10 fields everytime, because a Soup can have up to 10 SoupWord
/ For the new action, it should display 10 empty text fields.
/ Finally, if you fill three of the 10 fields,
/ model A should have only 3 instances of model B associated. i.e if there were 4 filled and
/ I set one of them blank, the model B instance should be destroyed.
= ff.text_field :word, placeholder: true
= f.submit
Controller:
class Bs < ApplicationController
def edit
respond_to :js
#soup = Soup.find params[:id]
end
def update
respond_to :js
puts params
end
end
Update
Create and edit actions now work, just put a reject_if parameter in model A,
accepts_nested_attributes_for :bs, reject_if: proc { |attrs| attrs[:word].blank? }
and set the build on the controller.
def new
respond_to :js
#a = A.new
10.times { #a.bs.build }
end
def edit
respond_to :js
#a = Soup.find params[:id]
#a.bs.size.upto(9) do |sw|
#a.bs.build
end
end
Now I need to destroy instances of model B if I set them blank in the edit action.
Normally you would delete nested records by using the allow_destroy: true option and by passing the _destroy param:
class Soup
accepts_nested_attributes_for :soup_words,
reject_if: proc { |attrs| attrs[:word].blank? },
allow_destroy: true
end
To get the behavior you want you can use javascript with a hidden input:
= form_for #soup, remote: true do |f|
= f.text_field :title, placeholder: true
= f.fields_for :soup_words, #soup.soup_words do |ff|
= ff.text_field :word, class: 'soup_word', placeholder: true
= ff.hidden_input :_destroy
= f.submit
$(document).on('change', '.soup_word', function(){
var $obj = $(this);
if (!this.value || !this.value.length) {
// set soup_word to be destroyed
$obj.siblings('input[name~=_destroy]').val('1');
}
$obj.fadeOut(50);
});
Make sure you have whitelisted the _destroy and id params.
def update_params
params.require(:soup).permit(:soup_words_attributes: [:word, :id, :_destroy])
end

Rails: Mapping a Date from form parameters into ActiveModel object

I have a filter-class which includes ActiveModel and consists of two dates:
class MealFilter
include ActiveModel::Model
attribute :day_from, Date
attribute :day_to, Date
end
That model is rendered into a form as following:
<%= form_for(#filter) do |f| %>
<div class="form-group form-group--date">
<%= f.label :day_from %>
<%= f.date_select :day_from %>
</div>
<div class="form-group form-group--date">
<%= f.label :day_to %>
<%= f.date_select :day_to %>
</div>
<% end %>
The problem is now, when the form gets submitted, it sends this parameters to the controller:
{"utf8"=>"✓", "meal_filter"=>{"day_from(1i)"=>"2016", "day_from(2i)"=>"1", "day_from(3i)"=>"29", "day_to(1i)"=>"2016", "day_to(2i)"=>"1", "day_to(3i)"=>"30"}, "commit"=>"Filter"}
I extract the values via Controller parameters:
def meal_filter_params
if params.has_key? :meal_filter
params.require(:meal_filter).permit(:day_from, :day_to)
end
end
if I now assign the params[:meal_filter] to my MealFilter class with #filter = MealFilter.new(meal_filter_params), my date fields are not updated correctly. It seams that the 1i, 2i, 3i parts are not correctly assigned to the dates.
However, this works fine if used an ActiveRecord class.
Do I miss some include? Does anyone know, where this magic mapping is implemented if not in ActiveModel::Model?
I came across this issue while upgrading an app from Rails 3.2 to 5.0
How I sorted it out is as follows:
For Rails 5
class MealFilter
include ActiveRecord::AttributeAssignment
attr_reader :day_from
def initialize(attributes = {})
self.attributes = attributes || {}
end
def day_from=(value)
#day_from = ActiveRecord::Type::Date.new.cast(value)
end
end
For Rails 4.2
class MealFilter
include ActiveRecord::AttributeAssignment
attr_accessor :day_from
def initialize(attributes = {})
self.attributes = attributes || {}
end
def type_for_attribute(name)
case name
when "day_from" then ActiveRecord::Type::Date.new
end
end
end
Then you can do:
attributes = { "day_from(3i)" => "1", "day_from(2i)" => "9", "day_from(1i)" => "2020" }
meal_filter = MealFilter.new(attributes)
meal_filter.day_from
# => Tue, 01 Sep 2020
Ok, found a solution.
What I needed was the MultiparameterAssignment which is actually implemented in ActiveRecord but not in ActiveModel.
As far as I can see, there is an open pull request (https://github.com/rails/rails/pull/8189) that should resolve this issue.
But in the meantime, some clever guy wrote a module that can be included into the model: https://gist.github.com/mhuggins/6c3d343fd800cf88f28e
All you need to do is to include the concern and define a class_for_attribute method that returns the class where your attribute should be mapped to - in my case Date.
You can simply access the 1i, 2i and 3i parameters from the date_select helper and combine them to make new Date in a before_validation callback:
class DateOfBirth
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Validations::Callbacks
attribute :date_of_birth, :date
attribute "date_of_birth(3i)", :string
attribute "date_of_birth(2i)", :string
attribute "date_of_birth(1i)", :string
before_validation :make_a_date
validates :date_of_birth, presence: { message: "You need to enter a valid date of birth" }
def make_a_date
year = self.send("date_of_birth(1i)").to_i
month = self.send("date_of_birth(2i)").to_i
day = self.send("date_of_birth(3i)").to_i
begin # catch invalid dates, e.g. 31 Feb
self.date_of_birth = Date.new(year, month, day)
rescue ArgumentError
return
end
end
end

mutliple select dropdown company and save perk and respective company

here is my code:
Perk not save on multiple select,when multiple true/false. perk save and habtm working.
class Perk < ActiveRecord::Base
has_and_belongs_to_many :companies
end
class Company < ActiveRecord::Base
has_and_belongs_to_many :perks
end
view perk/new.html.erb
<%= select_tag "company_id", options_from_collection_for_select(Company.all, 'id', 'name',#perk.companies.map{ |j| j.id }), :multiple => true %>
<%= f.text_field :name %>
Controller's code:
def new
#perk = Perk.new
respond_with(#perk)
end
def create
#perk = Perk.new(perk_params)
#companies = Company.where(:id => params[:company_id])
#perk << #companies
respond_with(#perk)
end
Your select_tag should return an array of company_ids:
<%= select_tag "company_ids[]", options_from_collection_for_select(Company.all, 'id', 'name',#perk.companies.map{ |j| j.id }), :multiple => true %>
http://apidock.com/rails/ActionView/Helpers/FormTagHelper/select_tag#691-sending-an-array-of-multiple-options
Then, in your controller, reference the company_ids param:
#companies = Company.where(:id => params[:company_ids])
(I assume that you've intentionally left out the #perk.save call in your create action... Otherwise, that should be included as well. Model.new doesn't store the record.)
It sounds like you may not have included company_id in the perk_params method in your controller. Rails four uses strong pramas this means you need to state the params you are allowing to be set.However it is difficult to say for sure without seeing more of the code.
In your controller you should see a method like this (there may be more options that just :name):
def perk_params
params.require(:perk).permit(:name)
end
You should try adding :company_id to it so it looks something like this:
def perk_params
params.require(:perk).permit(:name, :company_id)
end
if there are other params int your method leave them in and just added :company_id
EDIT to original answer
The above will only work on a one-to-many or one-to-one because you are using has_and_belongs_to_many you will need to add companies: [] to the end of your params list like this
def perk_params
params.require(:perk).permit(:name, companies: [] )
end
or like this
def perk_params
params.require(:perk).permit(:name, companies_ids: [] )
end
See these links for more details:
http://edgeapi.rubyonrails.org/classes/ActionController/StrongParameters.html
http://edgeguides.rubyonrails.org/action_controller_overview.html#strong-parameters

Getting rails3-autocomplete-jquery gem to work nicely with Simple_Form with multiple inputs

So I am trying to implement multiple autocomplete using this gem and simple_form and am getting an error.
I tried this:
<%= f.input_field :neighborhood_id, collection: Neighborhood.order(:name), :url => autocomplete_neighborhood_name_searches_path, :as => :autocomplete, 'data-delimiter' => ',', :multiple => true, :class => "span8" %>
This is the error I get:
undefined method `to_i' for ["Alley Park, Madison"]:Array
In my params, it is sending this in neighborhood_id:
"search"=>{"neighborhood_id"=>["Alley Park, Madison"],
So it isn't even using the IDs for those values.
Does anyone have any ideas?
Edit 1:
In response to #jvnill's question, I am not explicitly doing anything with params[:search] in the controller. A search creates a new record, and is searching listings.
In my Searches Controller, create action, I am simply doing this:
#search = Search.create!(params[:search])
Then my search.rb (i.e. search model) has this:
def listings
#listings ||= find_listings
end
private
def find_listings
key = "%#{keywords}%"
listings = Listing.order(:headline)
listings = listings.includes(:neighborhood).where("listings.headline like ? or neighborhoods.name like ?", key, key) if keywords.present?
listings = listings.where(neighborhood_id: neighborhood_id) if neighborhood_id.present?
#truncated for brevity
listings
end
First of all, this would be easier if the form is returning the ids instead of the name of the neighborhood. I haven't used the gem yet so I'm not familiar how it works. Reading on the readme says that it will return ids but i don't know why you're only getting names. I'm sure once you figure out how to return the ids, you'll be able to change the code below to suit that.
You need to create a join table between a neighborhood and a search. Let's call that search_neighborhoods.
rails g model search_neighborhood neighborhood_id:integer search_id:integer
# dont forget to add indexes in the migration
After that, you'd want to setup your models.
# search.rb
has_many :search_neighborhoods
has_many :neighborhoods, through: :search_neighborhoods
# search_neighborhood.rb
belongs_to :search
belongs_to :neighborhood
# neighborhood.rb
has_many :search_neighborhoods
has_many :searches, through: :search_neighborhoods
Now that we've setup the associations, we need to setup the setters and the attributes
# search.rb
attr_accessible :neighborhood_names
# this will return a list of neighborhood names which is usefull with prepopulating
def neighborhood_names
neighborhoods.map(&:name).join(',')
end
# we will use this to find the ids of the neighborhoods given their names
# this will be called when you call create!
def neighborhood_names=(names)
names.split(',').each do |name|
next if name.blank?
if neighborhood = Neighborhood.find_by_name(name)
search_neighborhoods.build neighborhood_id: neighborhood.id
end
end
end
# view
# you need to change your autocomplete to use the getter method
<%= f.input :neighborhood_names, url: autocomplete_neighborhood_name_searches_path, as: :autocomplete, input_html: { data: { delimiter: ',', multiple: true, class: "span8" } %>
last but not the least is to update find_listings
def find_listings
key = "%#{keywords}%"
listings = Listing.order(:headline).includes(:neighborhood)
if keywords.present?
listings = listings.where("listings.headline LIKE :key OR neighborhoods.name LIKE :key", { key: "#{keywords}")
end
if neighborhoods.exists?
listings = listings.where(neighborhood_id: neighborhood_ids)
end
listings
end
And that's it :)
UPDATE: using f.input_field
# view
<%= f.input_field :neighborhood_names, url: autocomplete_neighborhood_name_searches_path, as: :autocomplete, data: { delimiter: ',' }, multiple: true, class: "span8" %>
# model
# we need to put [0] because it returns an array with a single element containing
# the string of comma separated neighborhoods
def neighborhood_names=(names)
names[0].split(',').each do |name|
next if name.blank?
if neighborhood = Neighborhood.find_by_name(name)
search_neighborhoods.build neighborhood_id: neighborhood.id
end
end
end
Your problem is how you're collecting values from the neighborhood Model
Neighborhood.order(:name)
will return an array of names, you need to also collect the id, but just display the names
use collect and pass a block, I beleive this might owrk for you
Neighborhood.collect {|n| [n.name, n.id]}
Declare a scope on the Neighborhood class to order it by name if you like to get theat functionality back, as that behavior also belongs in the model anyhow.
edit>
To add a scope/class method to neighborhood model, you'd typically do soemthing like this
scope :desc, where("name DESC")
Than you can write something like:
Neighborhood.desc.all
which will return an array, thus allowing the .collect but there are other way to get those name and id attributes recognized by the select option.

Rails Admin modify list/show view to add new/customized column

I have setup the rails_admin for the admin interface of my site.
For one of the Model, I want to display an additional column.
say i have name , phone , email, image url, rank etc attributes in my Model(say Student).
Then I have to display columns : Name | Rank | Preview(additional column)
In the preview column i want to display some rendered html on the basis of attributes ( email,image,url etc.) for each 'student'.
I have found the way to include a partial for edit/update/create to provide fields/forms as per our partial. But the same implementation of including partial is failing in list/show.
So is there any way I can add the partial to show rendered content, in list/show view for a model...?
Edit: Code added
config.model Utility do
list do
field :code
field :priority
field :name
field :url
field :phone
field :logo
field :content
sort_by :priority
items_per_page 100
end
end
This shows up following columns in rails_admin
Code | Priority | Name | Url | Phone | Logo | Content
what i want is
Code | Priority | Preview
in which in Preview column i want to show a html rendering content as :
blah.html (just for e.g. html for example , here i want to render in a way it is displayed in one of pages, so it is presentable for admin view too)
<div class="blah">
<%=util.name%> <%=util.phone%> <%=util.logo%> #usage with proper divs/tags/rendering
</div >
config.model Utility do
configure :preview do
pretty_value do
util = bindings[:object]
%{<div class="blah">
#{util.name} #{util.phone} #{util.logo}
</div >}
end
children_fields [:name, :phone, :logo] # will be used for searching/filtering, first field will be used for sorting
read_only true # won't be editable in forms (alternatively, hide it in edit section)
end
list do
field :code
field :priority
field :preview
end
show do
field :code
field :priority
field :preview
end
# other sections will show all fields
end
Abstract:
Show/list don't use partials for output. Last overriding point is pretty_value.
Rails Admin calls these "virtual" field types. The easiest way is to make a method on your model, and then refer to it it in your list / show:
class ModelName < ActiveRecord::Base
def invite_link
%{invite link}.html_safe
end
rails_admin do
configure :invite_link do
visible false # so it's not on new/edit
end
list do
field :name
field :invite_link
end
show do
field :name
field :invite_link
end
end
end
class Utility < ActiveRecord::Base
def preview
name
end
end
config.model Utility do
configure :preview do
pretty_value do
util = bindings[:object]
%{<div class="blah">
#{util.name} #{util.phone} #{util.logo}
</div >}
end
children_fields [:name, :phone, :logo] # will be used for searching/filtering, first field will be used for sorting
read_only true # won't be editable in forms (alternatively, hide it in edit section)
end
list do
field :code
field :priority
field :preview
end
show do
field :code
field :priority
field :preview
end
# other sections will show all fields
end
class ModelName < ActiveRecord::Base
rails_admin do
list do
field :job_title
field :required_experiance
field :salary
field :technical_skills
field :non_technical_skills
end
create do
field :job_title, :enum do
help 'Please select Job Title'
enum do
['Business Analyst', 'Trainee Business Analyst', 'Mobile/Web Developer',
'iOS Developer', 'Graphic Designer', 'System Administrator', 'Content Writer']
end
end
field :job_type do
help 'e.g. Developer, Management'
end
field :undergraduate_degree, :enum do
help 'Please select UG Degree'
enum do
[ 'BE', 'BCA', 'B.Tech','BCs', 'BSc', 'BBA', 'BA', 'BCom', 'BSL']
end
end
field :postgraduate_degree, :enum do
help 'Please select PG Degree'
enum do
[ 'ME', 'MCA', 'M.Tech', 'MCs', 'MSc', 'MBA', 'MCM', 'MMM', 'MA', 'MCom']
end
end
field :required_experiance, :enum do
help 'Please select Year'
enum do
[ 'Select Year', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
end
end
end
end

Resources