Simple question, and I'm a little surprised that this isn't handled better in Rails already.
I am trying to scrub out some superfluous attributes from params in a number of Rails API controllers with the except!() method, like so:
params.except!( :format, :api_key, :controller, :action, :updated_at, :created_at )
Because these attributes are the same across a number of API endpoints, I wanted to store them in a Constant in the API's BaseController, like so:
In BaseController.rb
PARAMS_TO_SCRUB = [ :format, :api_key, :controller, :action, :updated_at, :created_at ]
params.except!( PARAMS_TO_SCRUB ) # => Doesn't work.
But the except!() method only accepts a splat of keys so none of the attributes get filtered:
# File activesupport/lib/active_support/core_ext/hash/except.rb, line 11
def except!(*keys)
keys.each { |key| delete(key) }
self
end
The work around I've setup now is to create a method in the BaseController that scrubs the params with the keys instead, like so:
def scrub_params
params.except!( :format, :api_key, :controller, :action, :updated_at, :created_at )
end
Is there no way to store a list of symbols like this?
Add * before array variable:
PARAMS_TO_SCRUB = [ :format, :api_key, :controller, :action, :updated_at, :created_at ]
params.except!( *PARAMS_TO_SCRUB )
So, the method will change to:
def scrub_params ex_arr
params.except! *ex_arr
end
Or some global or class variable.
Related
When I create auto documented API specification, I faced with problem of passing complex object (ActiveRecord for ex.) to param function of swagger-docs/swagger-ui_rails, because it takes only simple types (string, integer, ...).
I solved this trouble with next metaprogramming ruby trick:
class Swagger::Docs::SwaggerDSL
def param_object(klass, params={})
klass_ancestors = eval(klass).ancestors.map(&:to_s)
if klass_ancestors.include?('ActiveRecord::Base')
param_active_record(klass, params)
end
end
def param_active_record(klass, params={})
remove_attributes = [:id, :created_at, :updated_at]
remove_attributes += params[:remove] if params[:remove]
test = eval(klass).new
test.valid?
eval(klass).columns.each do |column|
unless remove_attributes.include?(column.name.to_sym)
param column.name.to_sym,
column.name.to_sym,
column.type.to_sym,
(test.errors.messages[column.name.to_sym] ? :required : :optional),
column.name.split('_').map(&:capitalize).join(' ')
end
end
end
end
Now I can use param_object for complex objects as param for simple types :
swagger_api :create do
param :id, :id, :integer, :required, "Id"
param_object('Category')
end
Git fork here:
https://github.com/abratashov/swagger-docs
How can I tell Ruby (Rails) to ignore protected variables which are present when mass-assigning?
class MyClass < ActiveRecord::Base
attr_accessible :name, :age
end
Now I will mass-assign a hash to create a new MyClass.
MyClass.create!({:name => "John", :age => 25, :id => 2})
This will give me an exception:
ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes: id
I want it to create a new MyClass with the specified (unprotected) attributes and ignore the id attribute.
On the side note: How can I also ignore unknown attributes. For example, MyClass doesn't have a location attribute. If I try to mass-assign it, just ignore it.
Use Hash#slice to only select the keys you're actually interested in assigning:
# Pass only :name and :age to create!
MyClass.create!(params.slice(:name, :age))
Typically, I'll add wrapper method for params to my controller which filters it down to only the fields that I know I want assigned:
class MyController
# ...
def create
#my_instance = MyClass.create!(create_params)
end
protected
def create_params
params.slice(:name, :age)
end
end
Setting mass_assignment_sanitizer to :logger solved the issue in development and test.
config.active_record.mass_assignment_sanitizer = :logger
You can use strong_parameters gem, that will be in rails 4.
See the documentation here.
This way you can specify the params you want by action or role, for example.
If you want to get down and dirty with it, and dynamically let only a model's attributes through, without disabling ActiveModel::MassAssignmentSecurity::Errors globally:
params = {:name => "John", :age => 25, :id => 2}
MyClass.create!(params.slice(*MyClass.new.attributes.symbolize_keys.keys)
The .symbolize_keys is required if you are using symbols in your hash, like in this situation, but you might not need that.
Personally, I like to keep things in the model by overriding assign_attributes.
def assign_attributes(new_attributes, options = {})
if options[:safe_assign]
authorizer = mass_assignment_authorizer(options[:as])
new_attributes = new_attributes.reject { |key|
!has_attribute?(key) || authorizer.deny?(key)
}
end
super(new_attributes, options)
end
Use it similarly to :without_protection, but for when you want to ignore unknown or protected attributes:
MyModel.create!(
{ :asdf => "invalid", :admin_field => "protected", :actual_data => 'hello world!' },
:safe_assign => true
)
# => #<MyModel actual_data: "hello world!">
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
I have a class Sample
Sample.class returns
(id :integer, name :String, date :date)
and A hash has all the given attributes as its keys.
Then how can I initialize a variable of Sample without assigning each attribute independently.
Something like
Sample x = Sample.new
x.(attr) = Hash[attr]
How can I iterate through the attributes, the problem is Hash contains keys which are not part of the class attributes too
class Sample
attr_accessor :id, :name, :date
end
h = {:id => 1, :name => 'foo', :date => 'today', :extra1 => '', :extra2 => ''}
init_hash = h.select{|k,v| Sample.method_defined? "#{k}=" }
# This will work
s = Sample.new
init_hash.each{|k,v| s.send("#{k}=", v)}
# This may work if constructor takes a hash of attributes
s = Sample.new(init_hash)
Take a look at this article on Object initialization. You want an initialize method.
EDIT You might also take a look at this SO post on setting instance variables, which I think is exactly what you're trying to do.
Try this:
class A
attr_accessor :x, :y, :z
end
a = A.new
my_hash = {:x => 1, :y => 2, :z => 3, :nono => 5}
If you do not have the list of attributes that can be assigned from the hash, you can do this:
my_attributes = (a.methods & my_hash.keys)
Use a.instance_variable_set(:#x = 1) syntax to assign values:
my_attributes.each do |attr|
a.instance_variable_set("##{attr.to_s}".to_sym, my_hash[attr])
end
Note(Thanks to Abe): This assumes that either all attributes to be updated have getters and setters, or that any attribute which has getter only, does not have a key in my_hash.
Good luck!
Using the new ActiveRecord::Store for serialization, the docs give the following example implementation:
class User < ActiveRecord::Base
store :settings, accessors: [ :color, :homepage ]
end
Is it possible to declare attributes with default values, something akin to:
store :settings, accessors: { color: 'blue', homepage: 'rubyonrails.org' }
?
No, there's no way to supply defaults inside the store call. The store macro is quite simple:
def store(store_attribute, options = {})
serialize store_attribute, Hash
store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
end
And all store_accessor does is iterate through the :accessors and create accessor and mutator methods for each one. If you try to use a Hash with :accessors you'll end up adding some things to your store that you didn't mean to.
If you want to supply defaults then you could use an after_initialize hook:
class User < ActiveRecord::Base
store :settings, accessors: [ :color, :homepage ]
after_initialize :initialize_defaults, :if => :new_record?
private
def initialize_defaults
self.color = 'blue' unless(color_changed?)
self.homepage = 'rubyonrails.org' unless(homepage_changed?)
end
end
I wanted to solve this too and ended up contributing to Storext:
class Book < ActiveRecord::Base
include Storext.model
# You can define attributes on the :data hstore column like this:
store_attributes :data do
author String
title String, default: "Great Voyage"
available Boolean, default: true
copies Integer, default: 0
end
end
try to use https://github.com/byroot/activerecord-typedstore gem. It allows you to set default value, use validation end other.
The following code has advantage of the defaults not being saved on every user record which reduces database storage usage and makes it easy in case if you want to change the defaults
class User < ApplicationRecord
DEFAULT_SETTINGS = { color: 'blue', homepage: 'rubyonrails.org' }
store :settings, accessors: DEFAULT_SETTINGS.keys
DEFAULT_SETTINGS.each do |key,value|
define_method(key) {
settings[key] or value
}
end
end
Here's what I just hacked together to solve this problem:
# migration
def change
add_column :my_objects, :settings, :text
end
# app/models/concerns/settings_accessors_with_defaults.rb
module SettingsAccessorsWithDefaults
extend ActiveSupport::Concern
included do
serialize :settings, Hash
cattr_reader :default_settings
end
def settings
self.class.default_settings.merge(self[:settings])
end
def restore_setting_to_default(key)
self[:settings].delete key
end
module ClassMethods
def load_default_settings(accessors_and_values)
self.class_variable_set '##default_settings', accessors_and_values
self.default_settings.keys.each do |key|
define_method("#{key}=") do |value|
self[:settings][key.to_sym] = value
end
define_method(key) do
self.settings[key.to_sym]
end
end
end
end
end
# app/models/my_object.rb
include SettingsAccessorsWithDefaults
load_default_settings(
attribute_1: 'default_value',
attribute_2: 'default_value_2'
)
validates :attribute_1, presence: true
irb(main):004:0> MyObject.default_settings
=> {:attribute_1=>'default_value', :attribute_2=>'default_value_2'}
irb(main):005:0> m = MyObject.last
=> #<MyObject ..., settings: {}>
irb(main):005:0> m.settings
=> {:attribute_1=>'default_value', :attribute_2=>'default_value_2'}
irb(main):007:0> m.attribute_1 = 'foo'
=> "foo"
irb(main):008:0> m.settings
=> {:attribute_1=>"foo", :attribute_2=>'default_value_2'}
irb(main):009:0> m
=> #<MyObject ..., settings: {:attribute_1=>"foo"}>