Related
How can I select a value from a pre-defined collection in a model, using collection_radio_buttons?
I tried
= f.collection_radio_buttons :lang_lvl, Language.language_levels, :language_level, :language_level #does not work
= f.select :lang_lvl, Language.language_levels, include_blank: true #works
The data source is defined in a model Language.rb:
class Languages < ActiveRecord::Base
LANGUAGE_LEVELS = [:"A1/Elem", :"A2/PreInt", :"B1/Int", :"B2/UpperInt", :"C1/Adv", :"C2/Prof", :"A1/Beg"]
def self.language_levels
LANGUAGE_LEVELS.map { |language_level| [language_level, language_level] }
end
end
The 3rd and 4th arguments of collection_radio_buttons specify the method used to extract value and text respectively. The function will try to call [language_level, language_level].language_level to convert the data because you're passing an array of arrays as a collection and :language_level to be called for each item in that sub-array.
If you don't want to change your model, you could try to change the code to this:
= f.collection_radio_buttons :lang_lvl, Language.language_levels, :first, :last
Suppose I have a class Article, such that:
class Article
attr_accessor :title, :author
def initialize(title, author)
#title = title
#author= author
end
end
Also, variable atrib is a String containing the name of an attribute. How could I turn this string into a variable to use as a getter?
a = Article.new
atrib='title'
puts a.eval(atrib) # <---- I want to do this
EXTENDED
Suppose I now have an Array of articles, and I want to sort them by title. Is there a way to do the compact version using & as in:
col = Article[0..10]
sorted_one = col.sort_by{|a| a.try('title') } #This works
sorted_two = col.sort_by(&:try('title')) #This does not work
You can use either send or instance_variable_get:
a = Article.new 'Asdf', 'Coco'
a.pubic_send(:title) # (Recommended) Tries to call a public method named 'title'. Can raise NoMethodError
=> "Asdf"
# If at rails like your case:
a.try :title # Tries to call 'title' method, returns `nil` if the receiver is `nil` or it does not respond to method 'title'
=> "Asdf"
a.send(:title) # Same, but will work even if the method is private/protected
=> "Asdf"
a.instance_variable_get :#title # Looks for an instance variable, returns nil if one doesn't exist
=> "Asdf"
Shot answer to your extended question: no. The &:symbol shortcut for procs relies on Symbol#to_proc method. So to enable that behavior you'd need to redifine that method on the Symbol class:
class Symbol
def to_proc
->(x) { x.instance_eval(self.to_s) }
end
end
[1,2,3].map(&:"to_s.to_i * 10")
=> [10, 20, 30]
ActiveRecord instances have an attributes hash:
a = Article.new(title: 'foo')
#=> <#Article id: nil, title: "foo">
atrib = 'title'
a.attributes[atrib]
#=> "foo"
You can use order to get sorted objects from your database:
Article.order('title').first(10)
#=> array of first 10 articles ordered by title
I make a http put request with following parameters:
{"post"=>{"files"=>{"file1"=>"file_content_1",
"file2"=>"file_content_2"}}, "id"=>"4"}
and i need to permit hash array in my code.
based on manuals I've tried like these:
> params.require(:post).permit(:files) # does not work
> params.require(:post).permit(:files => {}) # does not work, empty hash as result
> params.require(:post).permit! # works, but all params are enabled
How to make it correctly?
UPD1: file1, file2 - are dynamic keys
Rails 5.1+
params.require(:post).permit(:files => {})
Rails 5
params.require(:post).tap do |whitelisted|
whitelisted[:files] = params[:post][:files].permit!
end
Rails 4 and below
params.require(:post).tap do |whitelisted|
whitelisted[:files] = params[:post][:files]
end
In rails 5.1.2, this works now:
params.require(:post).permit(:files => {})
See https://github.com/rails/rails/commit/e86524c0c5a26ceec92895c830d1355ae47a7034
I understand that this is an old post. However, a Google search brought me to this result, and I wanted to share my findings:
Here is an alternative solution that I have found that works (Rails 4):
params = ActionController::Parameters.new({"post"=>{"files"=>{"file1"=>"file_content_1", "file2"=>"file_content_2"}}, "id"=>"4"})
params.require(:post).permit(files: params[:post][:files].keys)
# Returns: {"files"=>{"file1"=>"file_content_1", "file2"=>"file_content_2"}}
The difference between this answer and the accepted answer, is that this solution restricts the parameter to only 1 level of dynamic keys. The accepted answer permits multiple depths.
[Edit] Useful tip from comment
"Oh, and you need to verify that params[:post][.files] exists otherwise keys will fail"
Orlando's answer works, but the resulting parameter set returns false from the permitted? method. Also it's not clear how you would proceed if you were to later have other parameters in the post hash that you want included in the result.
Here's another way
permitted_params = params.require(:post).permit(:other, :parameters)
permitted_params.merge(params[:post][:files])
Here's what we had to do in Rails 5.0.0, hope this helps someone.
files = params[:post].delete(:files) if params[:post][:files]
params.require(:post).permit(:id).tap do |whitelisted|
whitelisted[:files] = files.permit!
end
In my case, there was just one attribute which had dynamic keys,
def post_params
marking_keys = Set.new
params[:post][:marking].keys.collect {|ii| marking_keys.add(ii)}
params.require(:post).permit(:name, marking: marking_keys.to_a)
end
Here is another way to get around this:
def post_params
permit_key_params(params[:post]) do
params.require(:post)
end
end
def permit_key_params(hash)
permitted_params = yield
dynamic_keys = hash.keys
dynamic_keys.each do |key|
values = hash.delete(key)
permitted_params[key] = values if values
end
permitted_params
end
This should work for post: { something: {...}, something_else: {...} }
You can use a temporary variable to build your permitted list like so:
permitted = params.require(:post).permit(:id)
permitted[:post][:files] = params[:post][:files].permit!
Here's a simple way to do it (works for rails 5):
def my_params
data_params = preset_data_params
params.require(:my_stuff).permit(
:some,
:stuff,
data: data_params
)
end
def preset_data_params
return {} unless params[:my_stuff]
return {} unless params[:my_stuff][:data]
params[:my_stuff][:data].keys
end
Send params as array type like name=date[]**strong text**
def user_post
dates = params[:date]
#render json: { 'response' => params }
i = 0
dates.each do |date|
locations = params['location_'+"#{i}"]
user_names = params['user_'+"#{i}"]
currency_rates = params['currency_'+"#{i}"]
flags = params['flag_'+"#{i}"]
j = 0
locations.each do |location|
User.new(user_name: user_names[j], currency_name: flags[j],
currency_rate: currency_rates[j], currency_flag: flags[j], location: location).save
j =+ 1
end
i =+ 1
end
def
I could not get any of the many proposed answers to work (Rails 5) without either:
knowing all the hash keys in advance, or
virtually negating the value of strong parameters by allowing arbitrary params.
I'm using this solution.
It uses the standard strong parameters rig to clean up most of the params,
and the Hash attribute is added back in explicitly.
# Assuming:
class MyObject < ApplicationRecord
serialize :hash_attr as: Hash
#...
end
# MyObjectsController method to filter params:
def my_object_params
# capture the hashed attribute value, as a Hash
hash_attr = params[:my_object] && params[:my_object][:hash_attr] ?
params[my_object][:hash_attr].to_unsafe_h : {}
# clean up the params
safe_params = params.require(:my_object).permit(:attr1, :attr2) # ... etc
# and add the hashed value back in
safe_params.to_unsafe_h.merge hash_attr: hash_attr
end
Let's use a more complicated subset of data:
task: {
code: "Some Task",
enabled: '1',
subtask_attributes: {
'1' => { field: 'something', rules: {length_10: true, phone: false, presence: false }} ,
'2' => { field: 'another', rules: {length_10: true, phone: false, presence: false }}
}
}
So we send it to Strong Parameters for processing:
params = ActionController::Parameters.new({
task: {
code: "Some Task",
enabled: '1',
subtask_attributes: {
'1' => { field: 'something', rules: {length_10: true, phone: false, presence: false }} ,
'2' => { field: 'another', rules: {length_10: true, phone: false, presence: false }}
}
}
})
We will not be able to specify :rules in Strong Params in Rails 4 because it is a hash of data:
permitted = params.require(:task).permit(:code, :enabled, subtask_attributes: [:field, :rules])
Unpermitted parameter: rules
Unpermitted parameter: rules
So what if you want to whitelist specific attributes AND a COLLECTION of hashes of data. The accepted answer does not whitelist specified attributes. You have to do this:
params.require(:task).permit(
:code, :enabled,
subtask_attributes: [:field, :rules],
)
# whitelist the validation rules hash
params.require(:task).tap do |whitelisted|
params[:task][:subtask_attributes].each do |k,v|
whitelisted[:subtask_attributes][k] = params[:task][:subtask_attributes][k]
whitelisted.permit!
end
end
After trying several of the solutions here, none worked. Only aboved worked for nested attributes in a has_many association which contains arbitrary hash data.
I know this is an old post, one of many with different ways to update a serialize hash field. I thought I give my version that I accidently found by piecing together some methods. I'll just use my application. This is Rails 7.0.4 and Ruby 3.0. I also use slim templates.
I have a Taxable model that contains semi-persistent tax rates for different Departments. All items are Sales Tax taxable, but in my case, Liquor adds an additional tax. The Taxable table only has two fields with tax being a serialized JSON field.
create_table "taxables", force: :cascade do |t|
t.date "date"
t.string "tax"
...
end
If a Tax is changed or added, the I would add a new record to reflect the change that took place on some date. Any ticket that had a tax in the past would use the record that is the earliest record before the ticket date. Anything new will the new changed record
The Taxable model has a constant that names all taxes that may be used:
TaxesUsed = %w(sales county federal city liquor)
The records would be something like:
[#<Taxable:0x0000000111c7bfc0
id: 2,
date: Sun, 01 Jan 2023,
tax: {"sales"=>"8.0", "county"=>"2.0", "federal"=>"0.0", "city"=>"0.0", "liquor"=>"3.0"} ...
#<Taxable:0x0000000111c7b980
id: 3,
date: Fri, 01 Jan 2021,
tax: {"sales"=>"8.0", "county"=>"2.0", "federal"=>"0.0", "city"=>"0.0", "liquor"=>"4.0"}...
]
I initially had a kludge that worked, which was creating the hash from some un-permitted parameter and updating the record. I then found mention of using form_with to describe the Tax field and to my surprise it worked! The form:
= form_with(model: #taxable) do |form|
div
= form.label :date, style: "display: block"
= form.date_field :date
div
= form.label :tax, style: "display: block", class:"font-bold"
= form.fields_for :tax do |tax|
# #taxable.tax is the existing serialize tax hash or a new default hash
- #taxable.tax.each do |k,v|
div.flex.gap-2
div.w-36.font-bold.text-right = k
div
= tax.text_field k, value:v
div[class="#{btn_submit}"]
= form.submit
I had to define a new taxable_parmam that states that :tax is a Hash
def taxable_params
params.require(:taxable).permit(:date, :tax => {})
end
Submitting the form give me params:
Parameters: {"authenticity_token"=>"[FILTERED]",
"taxable"=>{"date"=>"2021-01-01", "tax"=>{"sales"=>"8.0",
"county"=>"2.0", "federal"=>"0.0", "city"=>"0.0",
"liquor"=>"4.0"}}, "commit"=>"Update Taxable", "id"=>"3"}
and it works! I forgot about form_with but this is about a simple as you can get just using plain ol Rails.
Update: I forgot that stuff coming from form fields is text. I had to get the params to a new hash, change the float values (percents) and update using the new hash
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.
I have a place object that has the following parameters: phone, category, street, zip, website.
I also have an array of place objects: [place1, place2, place3, place4, place5].
What's the best way to sort the array of places, based on the parameter availability? I.e., if place1 has the most available parameters, or the least number of parameters that are nil, it should be reordered to first and so on.
Edit: These objects are not ActiveRecord objects
I'd let each Place object know how complete it was:
class Place
attr_accessor :phone, :category, :street, :website, :zip
def completeness
attributes.count{|_,value| value.present?}
end
end
Then it is easy to sort your place objects by completeness:
places.sort_by(&:completeness)
Edit: Non-ActiveRecord solution:
I had assumed this was an ActiveRecord model because of the Ruby on Rails tag. Since this is a non-ActiveRecord model, you can use instance_variables instead of attributes. (By the way, congratulations for knowing that domain models in Rails don't have to inherit from ActiveRecord)
class Place
attr_accessor :phone, :category, :street, :website, :zip
def completeness
instance_variables.count{|v| instance_variable_get(v).present?}
end
end
Edit 2: Weighted attributes
You have a comment about calculating a weighted score. In this case, or when you want to choose specific attributes, you can put the following in your model:
ATTR_WEIGHTS = {phone:1, category:1, street:2, website:1, zip:2}
def completeness
ATTR_WEIGHTS.select{|k,v| instance_variable_get(k).present?}.sum(&:last)
end
Note that the sum(&:last) is equivalent to sum{|k,v| v} which in turn is a railsism for reduce(0){|sum, (k,v)| sum += v}.
I'm sure there's a better way to do it, but this is a start :
ruby fat one liner
values = {phone: 5, category: 3, street: 5, website: 3, zip: 5} #Edit these values to ponderate.
array = [place1, place2, place3, place4, place5]
sorted_array = array.sort_by{ |b| b.attributes.select{ |k, v| values.keys.include?(k.to_sym) && v.present? }.inject(0){ |sum, n| sum + values[n[0]] } }.reverse
So we're basically creating a sub-hash of the attributes of your ActiveRecord object by only picking the key-value pairs that are in the values hash and only if they have a present? value.
Then on this sub-hash, we're invoking inject that will sum the ponderated values we've put in the values hash. Finally, we reverse everything so you have the highest score first.
To make it clean, I suggest you implement a method that will compute the score of each object in an instance method in your model, like mark suggested
If you have a class Place:
class Place
attr_accessor :phone, :category, :street, :website, :zip
end
and you create an instance place1:
place1 = Place.new
place1.instance_variables # => []
place1.instance_variables.size # => 0
place1.phone = '555-1212' # => "555-1212"
place1.instance_variables # => [ :#phone ]
place1.instance_variables.size # => 1
And create the next instance:
place2 = Place.new
place2.phone = '555-1212'
place2.zip = '00000'
place2.instance_variables # => [ :#phone, :#zip ]
place2.instance_variables.size # => 2
You can sort by an ascending number of instance variables that have been set:
[place1, place2].sort_by{ |p| p.instance_variables.size }
# => [ #<Place:0x007fa8a32b51a8 #phone="555-1212">, #<Place:0x007fa8a31f5380 #phone="555-1212", #zip="00000"> ]
Or sort in descending order:
[place1, place2].sort_by{ |p| p.instance_variables.size }.reverse
# => [ #<Place:0x007fa8a31f5380 #phone="555-1212", #zip="00000">, #<Place:0x007fa8a32b51a8 #phone="555-1212"> ]
This uses basic Ruby objects, Rails is not needed, and it asks the object instances themselves what is set, so you don't have to maintain any external lists of attributes.
Note: this breaks if you set an instance variable to something, then set it back to nil.
This fixes it:
[place1,place2].sort_by{ |p|
p.instance_variables.reject{ |v|
p.instance_variable_get(v).nil?
}.size
}.reverse
and this shortens it by using Enumerable's count with a block:
[place1,place2].sort_by{ |p|
p.instance_variables.count{ |v|
!p.instance_variable_get(v).nil?
}
}.reverse