let say I have model
User < ActiveRecord::Base
end
and his STI brother
MasqueradeUser < User
end
masquerade_user = MasqueradeUser.find 123
masquerade_user.class
# => MasqueradeUser
Ridiculous as it sounds, is possible to convert this object back to parent class User
masquerade_user.some_magic.class # => User
I know I can override methods like mode_name, is_a?(User) and other so that MasqueradeUser will return values like User
MasqueradeUser < User
def model_name
'User'
end
end
I was just wondering if there is a way to completely downgrade object to parent class instance
You can use becomes function of ActiveRecord - see here.
You can genericize ActiveRecord's becomes method as such:
def becomes(klass)
became = klass.new
became.instance_variable_set("#attributes", #attributes)
became
end
After all it's just a matter of copying variables into another object that supports them (i.e: it can "become" any class, not just the superclass)
Related
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).
I've created an instance variable on an ActiveRecord Model where I want to save a bit of computationally heavy data in each instance... Here's my code to do that:
class Account < ActiveRecord::Base
after_initialize :init
attr_accessor :market_value
def init
self.market_value ||= my_lengthy_function
end
end
where I'll take the hit to get that instance data (market_value) run when I init an instance of the model.
This works - I can see how I don't have to re-calculate my market_value property.
My problem is, when I access that object through another context, rails doesn't leverage that data as I'd expect.
For example:
Say I create an instance of an account (a = Account.find_by_id(2)). That market_value will be calculated on that object once.
If I have a nested has_many relationship to something called "holdings" (not in my sample code) on that account object, I'm going to want each of those holding objects (a holding) to be able to use it's parent account object.
However, in my code, I access the account from it's nested holding objects (my_holding.account.market_value) - I re-instantiate an instance of that account object, and incur that costly computation, even though it's already been computed.
How can I better leverage that account market_value property so that it doesn't keep recalculating?
I would not put the calculation logic in the ActiveRecord model. Maybe something along these lines:
class MarketValueCalculator
def initialize()
#market_values = {}
end
def calculate_for_account(account)
#market_values[account.id] ||= heavy_lifting
end
def heavy_lifting
###
end
end
#calculator = MarketValueCalculator.new
#market_value = #calculator.calculate_for_account(account)
#market_value = #calculator.calculate_for_account(my_holding.account)
i would build up a simple cache on class-level with the model ids as keys:
class Account < ActiveRecord::Base
def market_value
##market_value ||= {}
##market_value[id] ||= my_lengthy_function
end
end
not tested if this would work though, especially with class reloading in development.
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.
I have an ActiveRecord model with several virtual attribute setters. I want to build an object but not save it to the database. One setter must execute before the others. How to do?
As a workaround, I build the object in two steps
#other_model = #some_model.build_other_model
#other_model.setup(params[:other_model)
Where setup is:
class OtherModel < ActiveRecord::Base
def setup(other_params)
# execute the important_attribute= setter first
important_attribute = other_params.delete(:important_attribute)
# set the other attributes in whatever order they occur in the params hash
other_params.each { |k,v| self.send("#{k}=",v) }
end
end
This seems to work, but looks kludgy. Is there a better way?
EDIT
per neutrino's suggestion, I added a method to SomeModel:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = OtherModel.new(:some_model=>self)
other_model.setup(other_params)
other_model
end
end
It's a good thing that you have this manipulations done in an OtherModel's method, because you can just call this method and not worry about the order of assignments. So I would leave this part but just call it from a SomeModel's method:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = build_other_model
other_model.setup(other_params)
other_model
end
end
So then you would have
#other_model = #some_model.build_other_model(params[:other_model])
I took your idea of deleting the important attribute first in your setup method, but used alias_chain_method instead to make it more of a transparent process:
def attributes_with_set_important_attribute_first=(attributes = {})
# Make sure not to accidentally blank out the important_attribute when none is passed in
if attributes.symbolize_keys!.include?(:important_attribute)
self.important_attribute = attributes.delete(:important_attribute)
end
self.attributes_without_set_important_attribute_first = attributes
end
alias_method_chain :attributes=, :set_important_attribute_first
This way none of your code should change from the normal Rails style
#other_model = #some_model.other_models.build(params[:other_model])
Oddly enough, most of this works as it has been written, however I'm not sure how I can evaluate if the current_user has a badge, (all the relationships are proper, I am only having trouble with my methods in my class (which should partially be moved into a lib or something), regardless, the issue is specifically 1) checking if the current user has a record, and 2) if not create the corresponding new record.
If there is an easier or better way to do this, please share. The following is what I have:
# Recipe Controller
class RecipesController < ApplicationController
def create
# do something
if #recipe.save
current_user.check_if_badges_earned(current_user)
end
end
So as for this, it definitely seems messy, I'd like for it to be just check_if_badges_earned and not have to pass the current_user into the method, but may need to because it might not always be the current user initiating this method.
# User model
class User < ActiveRecord::Base
def check_if_badges_earned(user)
if user.recipes.count > 10
award_badge(1, user)
end
if user.recipes.count > 20
award_badge(2, user)
end
end
def award_badge(badge_id, user)
#see if user already has this badge, if not, give it to them!
unless user.badgings.any? { |b| b[:badge_id] == badge_id}
#badging = Badging.new(:badge_id => badge_id, :user_id => user)
#badging.save
end
end
end
So while the first method (check_if_badges_earned) seems to excucte fine and only give run award_badge() when the conditions are met, the issue happens in the award_badge() method itself the expression unless user.badgings.any? { |b| b[:badge_id] == badge_id} always evaluates as true, so the user is given the badge even if it already had the same one (by badge_id), secondly the issue is that it always saves the user_id as 1.
Any ideas on how to go about debugging this would be awesome!
Regardless of whether you need the current_user behavior above, award_badge should just be a regular instance method acting on self instead of acting on the passed user argument (same goes for check_if_badges_earned). In your award_badge method, try find_or_create_by_... instead of the logic you currently have. For example, try this:
class User < ActiveRecord::Base
# ...
def award_badge(badge_id)
badgings.find_or_create_by_badge_id(badge_id)
end
end
To access the current_user in your model classes, I sometimes like to use thread-local variables. It certainly blurs the separation of MVC, but sometimes this kind of coupling is just necessary in an application.
In your ApplicationController, store the current_user in a thread-local variable:
class ApplicationController < ActionController::Base
before_filter :set_thread_locals
private
# Store thread-local variables so models can access them (Hackish, but useful)
def set_thread_locals
Thread.current[:current_user] = current_user
end
end
Add a new class method to your ActiveRecord model to return the current_user (you could also extend ActiveRecord::Base to make this available to all models):
class User < ActiveRecord::Base
def self.current_user
Thread.current[:current_user]
end
end
Then, you'll be able to access the current user in the instance methods of your User model with self.class.current_user.
What you need to do first of all is make those methods class methods (call on self), which avoids needlessly passing the user reference.
Then, in your award_badge method, you should add the Badging to the user's list of Badgings, e.g.: user.badgings << Badging.new(:badge_id => badge_id)