Ruby/Rails convert string to a class attribute - ruby-on-rails

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

Related

How do you set up dynamic enumerables on Rails 5?

I'm trying to create a select input that takes specific User models and displays them as a string while saving them as an integer. Typically I would set the enumerable up the same way as below but as a static hash.
While attempting to create a new Product, I keep receiving the following error: "undefined local variable or method `user_business_hash'"
I've tried moving the 'user_business_hash' method to application/products controller, application/products helper with no luck.
Model
enum user_id: user_business_hash
validates :user_id, inclusion: user_ids.keys
def user_business_hash
# output: {User.business_name => User.id }
# ex: {"Business Name A"=>2, "Business Name B"=>1, "Business Name C"=>5}
array = User.where(account_type: 'Business').map{|x| [x.business_name, x.id] }
hash = array.inject({}) do |memo, (key, value)|
memo[key] = value
memo
end
return hash
end
Form
<%= form.select :user_id, Product.user_ids.keys, prompt: 'Select', id: :product_user_id %>
I think that what you actually want is:
on your controller
#options = User.where(account_type: 'Business')
on your view
options_from_collection_for_select(#options, "id", "business_name")

assigning multiple variable in one line with the hash returning from the method in ruby

I have a method that returns a hash map { :name => "Test", :desc => "Test Description }. It will always return :name and :description.
How can I assign 2 variables in with the returned hash.
I could do this but it will call the method twice:
#name, #desc = get_name_desc_map[:name], get_name_desc_map[:desc]
I only want to call the method once.
Very simple using Ruby's parallel assignment :
#name, #desc = get_name_desc_map.values
Other way is ( If you don't know the order of keys in the original hash ) :
#name, #desc = get_name_desc_map.values_at(:name, :desc)
Hash#values_at and Hash#values .

Ruby - Ignore protected attributes

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!">

ruby object array... or hash

I have an object now:
class Items
attr_accessor :item_id, :name, :description, :rating
def initialize(options = {})
options.each {
|k,v|
self.send( "#{k.to_s}=".intern, v)
}
end
end
I have it being assigned as individual objects into an array...
#result = []
some loop>>
#result << Items.new(options[:name] => 'name', options[:description] => 'blah')
end loop>>
But instead of assigning my singular object to an array... how could I make the object itself a collection?
Basically want to have the object in such a way so that I can define methods such as
def self.names
#items.each do |item|
item.name
end
end
I hope that makes sense, possibly I am overlooking some grand scheme that would make my life infinitely easier in 2 lines.
A few observations before I post an example of how to rework that.
Giving a class a plural name can lead to a lot of semantic issues when declaring new objects, as in this case you'd call Items.new, implying you're creating several items when in fact actually making one. Use the singular form for individual entities.
Be careful when calling arbitrary methods, as you'll throw an exception on any misses. Either check you can call them first, or rescue from the inevitable disaster where applicable.
One way to approach your problem is to make a custom collection class specifically for Item objects where it can give you the information you need on names and such. For example:
class Item
attr_accessor :item_id, :name, :description, :rating
def initialize(options = { })
options.each do |k,v|
method = :"#{k}="
# Check that the method call is valid before making it
if (respond_to?(method))
self.send(method, v)
else
# If not, produce a meaningful error
raise "Unknown attribute #{k}"
end
end
end
end
class ItemsCollection < Array
# This collection does everything an Array does, plus
# you can add utility methods like names.
def names
collect do |i|
i.name
end
end
end
# Example
# Create a custom collection
items = ItemsCollection.new
# Build a few basic examples
[
{
:item_id => 1,
:name => 'Fastball',
:description => 'Faster than a slowball',
:rating => 2
},
{
:item_id => 2,
:name => 'Jack of Nines',
:description => 'Hypothetical playing card',
:rating => 3
},
{
:item_id => 3,
:name => 'Ruby Book',
:description => 'A book made entirely of precious gems',
:rating => 1
}
].each do |example|
items << Item.new(example)
end
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
Do you know the Ruby key word yield?
I'm not quite sure what exactly you want to do. I have two interpretations of your intentions, so I give an example that makes two completely different things, one of them hopefully answering your question:
class Items
#items = []
class << self
attr_accessor :items
end
attr_accessor :name, :description
def self.each(&args)
#items.each(&args)
end
def initialize(name, description)
#name, #description = name, description
Items.items << self
end
def each(&block)
yield name
yield description
end
end
a = Items.new('mug', 'a big cup')
b = Items.new('cup', 'a small mug')
Items.each {|x| puts x.name}
puts
a.each {|x| puts x}
This outputs
mug
cup
mug
a big cup
Did you ask for something like Items.each or a.each or for something completely different?
Answering just the additional question you asked in your comment to tadman's solution: If you replace in tadman's code the definition of the method names in the class ItemsCollection by
def method_missing(symbol_s, *arguments)
symbol, s = symbol_s.to_s[0..-2], symbol_s.to_s[-1..-1]
if s == 's' and arguments.empty?
select do |i|
i.respond_to?(symbol) && i.instance_variables.include?("##{symbol}")
end.map {|i| i.send(symbol)}
else
super
end
end
For his example data you will get following outputs:
puts items.names.join(', ')
# => Fastball, Jack of Nines, Ruby Book
puts items.descriptions.join(', ')
# => Faster than a slowball, Hypothetical playing card, A book made entirely of precious gems
As I don't know about any way to check if a method name comes from an attribute or from another method (except you redefine attr_accessor, attr, etc in the class Module) I added some sanity checks: I test if the corresponding method and an instance variable of this name exist. As the class ItemsCollection does not enforce that only objects of class Item are added, I select only the elements fulfilling both checks. You can also remove the select and put the test into the map and return nil if the checks fail.
The key is the return value. If not 'return' statement is given, the result of the last statement is returned. You last statement returns a Hash.
Add 'return self' as the last line of initialize and you're golden.
Class Item
def initialize(options = {})
## Do all kinds of stuff.
return self
end
end

Custom method in model to return an object

In the database I have a field named 'body' that has an XML in it. The
method I created in the model looks like this:
def self.get_personal_data_module(person_id)
person_module = find_by_person_id(person_id)
item_module = Hpricot(person_module.body)
personal_info = Array.new
personal_info = {:studies => (item_module/"studies").inner_html,
:birth_place => (item_module/"birth_place").inner_html,
:marrital_status => (item_module/"marrital_status").inner_html}
return personal_info
end
I want the function to return an object instead of an array. So I can
use Module.studies instead of Model[:studies].
This is relatively simple; you're getting an Array because the code is building one. If you wanted to return an object, you'd do something like this:
class PersonalData
attr_accessor :studies
attr_accessor :birth_place
attr_accessor :marital_status
def initialize(studies,birth_place,marital_status)
#studies = studies
#birth_place = birth_place
#marital_status = marital_status
end
end
And your translation code would look like:
def self.get_personal_data_module(person_id)
person_module = find_by_person_id(person_id)
item_module = Hpricot(person_module.body)
personal_info = PersonalData.new((item_module/"studies").inner_html,
(item_module/"birth_place").inner_html,
(item_module/"marital_status").innner_html)
return personal_info
end
Or, if you want to avoid a model class, you could do something weird:
class Hash
def to_obj
self.inject(Object.new) do |obj, ary| # ary is [:key, "value"]
obj.instance_variable_set("##{ary[0]}", ary[1])
class << obj; self; end.instance_eval do # do this on obj's metaclass
attr_reader ary[0].to_sym # add getter method for this ivar
end
obj # return obj for next iteration
end
end
end
Then:
h = {:foo => "bar", :baz => "wibble"}
o = h.to_obj # => #<Object:0x30bf38 #foo="bar", #baz="wibble">
o.foo # => "bar"
o.baz # => "wibble"
It's like magic!
on a slightly different tack.
The idea of using a class method to do this feels wrong from an OO point of view.
You should really refactor this so that it works from an instance method.
def personal_data_module
item_module = Hpricot(body)
{
:studies => (item_module/"studies").inner_html,
:birth_place => (item_module/"birth_place").inner_html,
:marrital_status => (item_module/"marrital_status").inner_html
}
end
Then, where you need to use it, instead of doing....
Foobar.get_personal_data_module(the_id)
you would do
Foobar.find_by_person_id(the_id).personal_data_module
This looks worse, but in fact, thats a bit artificial, normally, you would be
referencing this from some other object, where in fact you would have a 'handle' on the person object, so would not have to construct it yourself.
For instance, if you have another class, where you reference person_id as a foreign key, you would have
class Organisation
belongs_to :person
end
then, where you have an organisation, you could go
organisation.person.personal_information_module
Yes, I know, that breaks demeter, so it would be better to wrap it in a delegate
class Organisation
belongs_to :person
def personal_info_module
person.personal_info_module
end
end
And then from controller code, you could just say
organisation.personal_info_module
without worrying about where it comes from at all.
This is because a 'personal_data_module' is really an attribute of that class, not something to be accessed through a class method.
But this also brings up some questions, for instance, is person_id the primary key of this table? is this a legacy situation where the primary key of the table is not called 'id'?
If this is the case, have you told ActiveRecord about this or do you have to use 'find_by_person_id' all over where you would really want to write 'find'?

Resources