How to access class variable in Model class - ruby-on-rails

I want to define the class variable test, threshold
so that I can use Order.test, Order.threshold in my Rails app
but I can not access the class variable when using the rails console
I must misunderstand something, where's the problem? Thanks.
class Order < ActiveRecord::Base
##test=123
##threshold = {
VIP: 500,
PLATINUM: 20000
}
Here is the rails console
irb(main):001:0> Order.class_variables
=> [:##test, :##threshold]
irb(main):002:0> Order.test
NoMethodError: private method `test' called for #<Class:0x007fe5a63ac738>

Do this:
class Order < ActiveRecord::Base
cattr_reader :test, :threshold
self.test = 123
self.threshold = {
VIP: 500,
PLATINUM: 20000
}
end
Order.test
Or I'd use constants:
class Order < ActiveRecord::Base
TEST = 123
end
Order::TEST

I would just use class methods:
class Order < ActiveRecord::Base
def self.test
123
end
def self.threshold
{ VIP: 500, PLATINUM: 20000 }
end
end
Constants would work, as well, but if you already have code that expects Order.test and Order.threshold to exist, you'd have to change your code to call the constant instead. Plus, Avdi Grimm gives some good reasons for using methods instead of constants in a blog post.
The reason accessing class variables won't work the way you expected is that Ruby restricts access to variables like this. You need to either define accessor methods directly (like self.test or self.threshold), or indirectly using something like cattr_reader. You could also use cattr_accessor if you need to write to the variables from the outside world.
I would generally recommend avoiding class variables, though. They have some unintuitive behavior.

Related

Ruby MyClass = Class.new autoloading best practice

I have class defined like this, and I been told this could cause problems with Rails autoload. What is the reason behind this? and when should we use Class.new?
class Integration < ActiveRecord::Base
MYobIdentifier = Class.new(Integration)
end
If a constant (in this case your class name) cannot be find then Ruby will attempt to find it using a filename based on the constant name. In this case it will search for MYobIdentifier in a file called m_yob_identifier (There are a few different places it would look for this file)
As long as you can be sure that the Integration class is loaded before any attempt is made to ys MYobIdentifier then it will always be defined and you do not need to worry.
Also in your case, I assume you will access it as Integration::MYobIdentifier in which case Integration will always be loaded before trying to resolve the constant.
Note: I think you would be better off capitalizing the class as MyObIdentifier
As the stackoverflow question linked in a comment covers,
class A
class B
end
end
becomes this under the hood:
A = Class.new do
B = Class.new
end
One additional detail of your example is that another class Integration is being passed as an argument to Class.new. This sets up inheritance. You can check whether one class inherits from another using ClassA < ClassB; this returns true or nil:
class A
class B
end
end
A::B < A
# => nil
class A
B = Class.new(A)
end
A::B < A
# => true
this second example is the same as this:
class A
class B < A
end
end
A::B < A
# => true

How to call a parent class and instead create one of it's children?

I have a model directory structure like this:
/alerts
base_alert.rb
panic_alert.rb
hardware_alert.rb
alert.rb
With the /alerts/x_alert.rb models setup like this:
class base_alert < ActiveRecord::Base
...
end
class panic_alert < base_alert
...
end
class hardware_alert < base_alert
...
end
etc.
Is there any way to call create on alert.rb in the top directory, and, based on a parameter passed, it would create one of the children instead of alert.rb.
I.E. Alert.create({type:"panic_alert"})
And it would create and return one of the panic_alert types of alerts?
By making few changes to the class definitions, like subclassing the Alert from ActiveRecord::Base rather than BaseAlert, you could achieve what you are trying to accomplish.
Following are the updated classes:
# app/models/alert.rb
class Alert < ActiveRecord::Base
end
# app/models/alerts/base_alert.rb
module Alerts
class BaseAlert < ::Alert
end
end
# app/models/alerts/panic_alert.rb
module Alerts
class PanicAlert < BaseAlert
end
end
# app/models/alerts/hardware_alert.rb
module Alerts
class HardwareAlert < BaseAlert
end
end
Following are few ways to create the subclasses from the base class:
#panic_alert = Alert.create!(
type: 'Alerts::PanicAlert', #this has to be string
#other attributes
)
#alert = Alert.new
#alert.type = 'Alerts::PanicAlert' #this has to be string
# assign other attributes, if necessary
#alert.save
#alert = Alert.new
#panic_alert = #alert.becomes(Alerts::PanicAlert) #this has to be class
# assign other attributes, if necessary
#panic_alert.save
You can use the constantize or the safe_constantize methods to do that. What they do is take a string and try to return the class the string refers to. For instance:
"BaseAlert".safe_constantize
=> BaseAlert
or
def method_name(alert_type)
alert_type.safe_constantize.create()
end
The difference between the two is constantize will throw an error if there isn't a match for the string, while safe_constantize will just return nil. Remember, if you pass in a underscored string (say panic_alert) then you would have to camelize it.
What seems like a lifetime ago I created StiFactory for this. That said, I don't find much use for STI these days (hence the lack of maintenance).

ActiveSupport::Concern and class methods

I'm using ActiveSupport::Concern to dry up some code that is included on my AR classes. I have a module for calculating the lower wilson bound on data:
module CalculateWilsonBound
extend ActiveSupport::Concern
included do
class_attribute :wilson_ratings
class_attribute :wilson_positive_ratings
end
def calculate_wilson_lower_bound
number_of_ratings = self.class.wilson_ratings.call(self)
...
end
end
After I've included it onto an object, I want to provide two class level methods (wilson_ratings, wilson_positive_ratings) which define blocks which will return respective counts.
From the AR objects point of view:
class Influence < ActiveRecord::Base
include CalculateWilsonBound
wilson_ratings { |model| model.votes }
wilson_positive_ratings { |model| model.positive_votes }
This does not cause any runtime errors, but when I got to access the class attribute:
number_of_ratings = self.class.wilson_ratings.call(self)
It's nil.
First of all, am I organising the code in a that makes sense, secondaly, why is the class attribute nil?
I believe you will need to do:
class Influence < ActiveRecord::Base
include CalculateWilsonBound
self.wilson_ratings = Proc.new { |model| model.votes }
self.wilson_positive_ratings = Proc.new { |model| model.positive_votes }
end
At the moment you have 2 problems.
When trying to assign a class attribute in the context of a class definition Rails will not realise that you are referring to the class attribute unless you use self.
You need to assign the class attribute, rather than pass a block into it. As your code reads now it looks like you are calling a method called wilson_ratings and passing a block into it.
As to whether your code is sensible, it is starting to smell a little funny to me. I much prefer the service class pattern where possible (see http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/) - it prevents you from having to mess around with class_attributes and other potentially hairy concepts.
If you want to make class attribute callable, you can assign it a proc
self.wilson_ratings = proc{ |model| model.votes }

What is the proper way to access class variables in Ruby 1.9?

I'm trying to set some class variables to store paths in a Rails application (but I think this more a ruby question)
Basically my class looks like this
class Image < ActiveRecord::Base
##path_to_folder = "app/assets"
##images_folder = "upimages"
##path_to_images = File.join(##path_to_folder, ##images_folder)
end
But when I try to access ##path_to_images from my controller by doing Image.path_to_images, I get a NoMethodError
When I try with Image.class_eval( ##path_to_images ), I get uninitialized class variable ##path_to_images in ImagesController
I've searched around and all I've seen says those would work, so I'm very confused about this
What's more, I tried defining simple classes with the ruby console like so
class Bidule
##foo = "foo"
Bar = "bar"
end
And so I tried, I think, all the ways possible (previous 2 included) to access them but no way I always get an exception raised
Rails provides class level attribute accessor for this functionality
Try
class Image < ActiveRecord::Base
cattr_accessor :path_to_folder
##path_to_folder = "app/assets"
end
Then to access path_to_folder class variable just use
Image.path_to_folder
But people always suggest to avoid class variables due to its behavior in inheritance.So you can use constants like
class Image < ActiveRecord::Base
PATH_TO_FOLDER = "app/assets"
end
Then you can access the constant like
Image::PATH_TO_FOLDER
Although I wouldn't in general recommend it, you can access class variables by passing a string to class_eval as in:
Image.class_eval('##path_to_folder')
at least in later versions of Ruby.
Note, however, that class variables are associated with the uppermost class in a subclassed hierarchy. In the case of ActiveRecord subclasses like this one, this means that these class variables really exist in the namespace of ActiveRecord::Base.
If you can't or don't want to extend class use:
Image.class_variable_get(:##path_to_images)
Best way is to set and get the value using methods. Below is a sample code
class Planet
##planets_count = 0
def initialize(name)
#name = name
##planets_count += 1
end
def self.planets_count
##planets_count
end
def self.add_planet
##planets_count += 1
end
def add_planet_from_obj
##planets_count += 1
end
end
Planet.new("uranus")
Plant.add_planet
obj = Planet.new("earth")
obj.add_planet_from_obj
Class variables are rarely used in Ruby applications because they have a lot of limitations and also tend to run against the grain of proper Object-Oriented design.
In nearly every case a class variable can be replaced with a proper constant, a class method, or both.
Your example is probably better described as:
class Image < ActiveRecord::Base
PATH_TO_FOLDER = "app/assets"
IMAGES_FOLDER = "upimages"
PATH_TO_IMAGES = File.join(PATH_TO_FOLDER, IMAGES_FOLDER)
end
Class variables are private to the class in question and don't trickle down to sub-classes and are difficult to access from an external context. Using constants allows the use of things like:
image_path = Image::PATH_TO_FOLDER
There are some circumstances under which a class variable is more reasonable than the alternative, but these are usually very rare.
You can do that by wrapping it in a class method, like this:
def self.path_to_images
##path_to_images
end
but I should mention that you should try to avoid using class variables in rails
I would modify it like this:
class Image < ActiveRecord::Base
##path_to_folder = "app/assets"
##images_folder = "upimages"
##path_to_images = File.join(##path_to_folder, ##images_folder)
def initialize
end
...
def self.path_to_folder
##path_to_folder
end
end
What you've done here is make the class variable into a method, so you can now access is using a .method name call. Since this is a class variable though, you can only call this on the class itself, not on the instance of a class.
You are getting a 'NoMethodError' because you're calling the class variable using a method which has not exist. The code above defines this method on the lines where you say:
def self.path_to_folder
##path_to_folder
end
This will now work:
Image.path_to_folder

Dynamically instantiate Rails nested STI subclass?

Let's say I have a class like:
class Basket < ActiveRecord::Base
has_many :fruits
Where "fruits" is an STI base class having subclasses like "apples", "oranges", etc...
I'd like to be able to have a setter method in Basket like:
def fruits=(params)
unless params.nil?
params.each_pair do |fruit_type, fruit_data|
fruit_type.build(fruit_data)
end
end
end
But, obviously, I get an exception like:
NoMethodError (undefined method `build' for "apples":String)
A workaround I thought of works like this:
def fruits=(params)
unless params.nil?
params.each_pair do |fruit_type, fruit_data|
"#{fruit_type}".create(fruit_data.merge({:basket_id => self.id}))
end
end
end
But that causes the Fruit STI object to be instantiated before the Basket class, and so the basket_id key is never saved in the Fruit subclass (because basket_id doesn't exist yet).
I'm totally stumped. Anyone have any ideas?
Instead of adding a setter method in Basket, add it in Fruit:
class Fruit < ActiveRecord::Base
def type_setter=(type_name)
self[:type]=type_name
end
end
Now you can pass the type in when you build the object through an association:
b = Basket.new
b.fruits.build(:type_setter=>"Apple")
Note that you can't assign :type this way, since it is protected from mass assignment.
EDIT
Oh, you wanted to run different callbacks depending on the subclass? Right.
You could do this:
fruit_type = "apples"
b = Basket.new
new_fruit = b.fruits << fruit_type.titleize.singularize.constantize.new
new_fruit.class # Apple
or define a has_many association for each type:
require_dependency 'fruit' # assuming Apple is defined in app/models/fruit.rb
class Basket
has_many :apples
end
then
fruit_type = "apples"
b = Basket.new
new_fruit = b.send(fruit_type).build
new_fruit.class # Apple
In Ruby terms, "#{x}" is simply equivalent to x.to_s which for String values is exactly the same as the string itself. In other languages, like PHP, you can de-reference a string and treat it as a class, but that's not the case here. What you probably mean is this:
fruit_class = fruit_type.titleize.singularize.constantize
fruit_class.create(...)
The constantize method converts from a string to the equivalent class, but it is case sensitive.
Keep in mind that you're exposing yourself to the possibility someone might create something with fruit_type set to "users" and then go ahead and make an administrator account. What's perhaps more responsible is to do an additional check that what you're making is actually of the right class.
fruit_class = fruit_type.titleize.singularize.constantize
if (fruit_class.superclass == Fruit)
fruit_class.create(...)
else
render(:text => "What you're doing is fruitless.")
end
One thing to watch out for when loading classes this way is that constantize will not auto-load classes like having them spelled out in your application does. In development mode you may be unable to create subclasses that have not been explicitly referenced. You can avoid this by using a mapping table which solves the potential security problem and pre-loading all at once:
fruit_class = Fruit::SUBCLASS_FOR[fruit_type]
You can define this constant like this:
class Fruit < ActiveRecord::Base
SUBCLASS_FOR = {
'apples' => Apple,
'bananas' => Banana,
# ...
'zuchini' => Zuchini
}
end
Using the literal class constant in your model will have the effect of loading them immediately.

Resources