Subclassing ActiveRecord with permalink_fu in a rails engine - ruby-on-rails

This question is related to extending class methods in Ruby, perhaps more specifically in the way that permalink_fu does so.
It appears that has_permalink on a model will not be available in a derived model. Certainly I would expect anything defined in a class to be inherited by its derived classes.
class MyScope::MyClass < ActiveRecord::Base
unloadable
self.abstract_class = true
has_permalink :name
end
class MyClass < MyScope::MyClass
unloadable
#has_permalink :name # This seems to be required
end
Is there something in the way permalink_fu mixes itself in that causes this issue?
I'm using the permalink-v.1.0.0 gem http://github.com/goncalossilva/permalink_fu

After investigating this, I can now see that the problem is related to how permalink_fu verifies it it should create a permalink or not. It verifies this by checking if the permalink_field of the class is blank or not.
What's the permalink_field? When you do
class Parent < ActiveRecord::Base
has_permalink :name
end
class Child < Parent
end
you can access the permalink by writing Parent.new.permalink or Child.new.permalink. This method name can be changed by writing
class Parent < ActiveRecord::Base
has_permalink :name 'custom_permalink_name'
end
If so, the permalink will be accessible by writing Parent.new.custom_permalink_name (or Child.new.custom_permalink_name).
What's the problem with this? The permalink_field accessor methods are defined on Parent's metaclass:
class << self
attr_accessor :permalink_field
end
When you run the has_permalink method, it calls Parent.permalink_field = 'permalink'.
The problem is that although the permalink_field method is available on all subclasses, its value is stored on the class it was called. This means that the value is not propagated to the subclasses.
So, as the permalink_field is stored on the Parent class, the Child does not inherit the value, although it inherits the accessor methods. As Child.permalink_field is blank, the should_create_permalink? returns false, and Child.create :name => 'something' does not create a permalink.
A possible solution would be to replace the attr_acessors on the metaclass with cattr_accessors on the class (lines 57 to 61 on the permalink_fu.rb file).
Replace
class << base
attr_accessor :permalink_options
attr_accessor :permalink_attributes
attr_accessor :permalink_field
end
with
base.cattr_accessor :permalink_options
base.cattr_accessor :permalink_attributes
base.cattr_accessor :permalink_field
Note that this will invalidate any possible customization on the subclass. You will no longer be able to specify different options for the subclasses, as these three attributes are shared by Parent and all its subclasses (and subsubclasses).

Related

Set non-database attribute for rails model without `attr_accessor`

In PHP, I can set an attribute (that is not a column in database) to a model. E.g.(PHP code),
$user = new User;
$user->flag = true;
But in rails, when I set any attribute that doesn't exist in database, it will throw an error undefined method flag. There is attr_accessor method, but what will happen if I need about ten temp attributes?
but what will happen if I need about ten temp attributes?
#app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :flag, :other_attribute, :other_attribute2, :etc...
end
attr_accessor creates "virtual" attributes in Rails -- they don't exist in the database, but are present in the model.
As with attributes from the db, attr_accessor just creates a set of setter & getter (instance) methods in your class, which you can call & interact with when the class is initialized:
#app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :flag
# getter
def flag
#flag
end
# setter
def flag=(val)
#flag = val
end
end
This is expected because it's how ActiveRecord works by design. If you need to set arbitrary attributes, then you have to use a different kind of objects.
For example, Ruby provides a library called OpenStruct that allows you to create objects where you can assign arbitrary key/values. You may want to use such library and then convert the object into a corresponding ActiveRecord instance only if/when you need to save to the database.
Don't try to model ActiveRecord to behave as you just described because it was simply not designed to behave in that way. That would be a cargo culting error from your current PHP knowledge.
As the guys explained, attr_accessor is just a quick setter and getter.
We can set our Model attr_accessor on record initializing to be a Ruby#Hash for example using ActiveRecord#After_initilize method so we get more flexibility on temporarily storing values (idea credit to this answer).
Something like:
class User < ActiveRecord::Base
attr_accessor :vars
after_initialize do |user|
self.vars = Hash.new
end
end
Now you could do:
user = User.new
#set
user.vars['flag'] = true
#get
user.vars['flag']
#> true
All that attr_accessor does is add getter and setter methods which use an instance variable, eg this
attr_accessor :flag
will add these methods:
def flag
#flag
end
def flag=(val)
#flag = val
end
You can write these methods yourself if you want, and have them do something more interesting than just storing the value in an instance var, if you want.
If you need temp attributes you can add them to the singleton object.
instance = Model.new
class << instance
attr_accessor :name
end

Passing in a hash to a class without using an initialization method

I was under the, apparently incorrect, impression that when I pass a hash into a class the class requires an initialization method like this:
class Dog
attr_reader :sound
def initialize(params = {})
#sound = params[:sound]
end
end
dog = Dog.new({sound:"woof"})
puts dog.sound
But I've run into a bit of code (for creating a password digest) that works within a rails application that doesn't use an initialization method and seems to work just fine and it's kind of confuses me because when I try this anywhere else it doesn't seem to work. Here is the sample code that works (allows me to pass in a hash and initializes without an initialization method):
class User < ActiveRecord::Base
attr_reader :password
validates :email, :password_digest, presence: true
validates :password, length: { minimum: 6, allow_nil: true }
def password=(pwd)
#password = pwd
self.password_digest = BCrypt::Password.create(pwd)
end
end
NOTE: In the create action I pass in a hash via strong params from a form that, at the end of the day, looks something like this {"email"=>"joeblow#gmail.com", "password"=>"holymolycanoli”}
In this bit of code there is no initialization method. When I try something like this (passing in a hash without an initialization method) in pry or in a repl it doesn't seem to work (for instance the following code does not work):
class Dog
attr_reader :sound
def sound=(pwd)
#sound = pwd
end
end
dog = Dog.new({sound:"woof"})
puts dog.sound
The error I get is:
wrong number of arguments (1 for 0)
Is it rails that allows me to pass in hashes like this or ActiveRecord? I'm confused as to why it works within rails within this context but generates an error outside of rails. Why does this work in rails?
If you look at the top you have this:
class Dog < ActiveRecord::Base
This causes your class Dog to inherit from ActiveRecord::Base
when it does so it gains a bunch of methods that allows you to set things up.
Now when you call for example:
Dog.create(password: 'some_password', username: 'some_username')
Your calling a method on the class object that then returns an instance of the class.
so taking your example
class Dog
attr_reader :sound
def sound=(pwd)
#sound = pwd
end
def self.create data_hash
new_dog = self.new #create new instance of dog class
new_dog.sound = data_hash[:sound] #set instance of dog classes sound
new_dog # return instance of dog class
end
end
It's essentially what we would term a factory method, a method that takes in data and returns an object based on that data.
Now I have no doubt that ActiveRecord::Base is doing something more complicated than that but that is essentially what it's doing at the most basic of levels.
I'd also like to point out that when inheriting from ActiveRecord::Base your also inheriting its 'initialize' method so you don't have to set one yourself.
The class knows what attribute methods to create based on the schema you set when you did the DB migrations for a table that matches (through rail's conventions) the class.
A lot of things happen when you subclass ActiveRecord::Base. Before looking at other issues I'm guessing that Dog is a rails ActiveRecord model and you just forgot to add
class Dog < ActiveRecord::Base

Class accessors and inheritance in Ruby on Rails

Like the following code shows, having class accessors defined in the superclass could have unexpected behaviours because the class accessor is the same variable for all the subclasses.
class Super
cattr_accessor :name
end
class SubA < Super; end
class SubB < Super; end
SubA.name = "A"
SubB.name = "B"
SubA.name
=> "B" # unexpected!
I want to have an independent class accessor for each subclass, so a possible solution is moving the cattr_accessor from the superclass and putting it in every subclass.
class Super; end
class SubA < Super
cattr_accessor :name
end
class SubB < Super
cattr_accessor :name
end
SubA.name = "A"
SubB.name = "B"
SubA.name
=> "A" # expected!
It is this solution a good practice? Do you know better alternatives?
Open up Super's singleton class and give that a regular attr_accessor:
class Super
class << self
attr_accessor :name
end
end
That should give you the semantics you want: a "class level instance variable".
However I'll note that any value set for :name on Super will not be inherited by Super's children. This makes sense if you think about it: the children inherit the attr_accessor, not the attribute itself.
There are some ways around this, most notably rails provides class_attribute which provides the ability of children to inherit the value of the parent's attribute unless explicitly overridden.

How to run validations of sub-class in Single Table Inheritance?

In my application, I have a class called Budget. The budget can be of many types.. For instance, let's say that there are two budgets: FlatRateBudget and HourlyRateBudget. Both inherit from the class Budget.
This is what I get so far:
class Budget < ActiveRecord::Base
validates_presence_of :price
end
class FlatRateBudget < Budget
end
class HourlyRateBudget < Budget
validates_presence_of :quantity
end
In the console, if I do:
b = HourlyRateBudget.new(:price => 10)
b.valid?
=> false
b.errors.full_messages
=> ["Quantity can't be blank"]
As, expected.
The problem is that the "type" field, on STI, comes from params.. So i need to do something like:
b = Budget.new(:type => "HourlyRateBudget", :price => 10)
b.valid?
=> true
Which means that rails is running validations in the super-class instead of instantiating the sub class after I set up the type.
I know that is the expected behaviour, since I'm instantiating a class that dosen't need the quantity field, but I wonder if there is anyway to tell rails to run the validations for the subclass instead of the super.
You could probably solve this with a custom validator, similar to the answer on this question: Two models, one STI and a Validation However, if you can simply instantiate the intended sub-type to begin with, you would avoid the need for a custom validator altogether in this case.
As you've noticed, setting the type field alone doesn't magically change an instance from one type to another. While ActiveRecord will use the type field to instantiate the proper class upon reading the object from the database, doing it the other way around (instantiating the superclass, then changing the type field manually) doesn't have the effect of changing the object's type while your app is running - it just doesn't work that way.
The custom validation method, on the other hand, could check the type field independently, instantiate a copy of the appropriate type (based on the value of the type field), and then run .valid? on that object, resulting in the validations on the sub-class being run in a way that appears to be dynamic, even though it's actually creating an instance of the appropriate sub-class in the process.
I've done something similar.
Adapting it to your problem:
class Budget < ActiveRecord::Base
validates_presence_of :price
validates_presence_of :quantity, if: :hourly_rate?
def hourly_rate?
self.class.name == 'HourlyRateBudget'
end
end
For anyone looking for example code, here's how I implemented the first answer:
validate :subclass_validations
def subclass_validations
# Typecast into subclass to check those validations
if self.class.descends_from_active_record?
subclass = self.becomes(self.type.classify.constantize)
self.errors.add(:base, "subclass validations are failing.") unless subclass.valid?
end
end
Instead of setting the type directly set the type like that... Instead, try:
new_type = params.fetch(:type)
class_type = case new_type
when "HourlyRateBudget"
HourlyRateBudget
when "FlatRateBudget"
FlatRateBudget
else
raise StandardError.new "unknown budget type: #{new_type}"
end
class_type.new(:price => 10)
You could even transform the string into its class by:
new_type.classify.constantize but if it's coming in from params, that seems a bit dangerous.
If you do this, then you'll get a class of HourlyRateBudget, otherwise it'll just be Budget.
Better yet, use type.constantize.new("10"), however this depends on that the type from params must be correct string identical to HourlyRateBudget.class.to_s
I also required the same and with the help of Bryce answer i did this:
class ActiveRecord::Base
validate :subclass_validations, :if => Proc.new{ is_sti_supported_table? }
def is_sti_supported_table?
self.class.columns_hash.include? (self.class.inheritance_column)
end
def subclass_validations
subclass = self.class.send(:compute_type, self.type)
unless subclass == self.class
subclass_obj= self.becomes(subclass)
self.errors.add(:base, subclass_obj.errors.full_messages.join(', ')) unless subclass_obj.valid?
end
end
end
Along the lines of #franzlorenzon's answer, but using duck typing to avoid referencing class type in the super class:
class Budget < ActiveRecord::Base
validates_presence_of :price
validates_presence_of :quantity, if: :hourly_rate?
def hourly_rate?
false
end
end
class HourlyRateBudget < Budget
def hourly_rate?
true
end
end

How to add property in module in Rails CouchRest Model to support multiple inheritance?

In my class, I want to include multiple modules. Each module can
define its own property to persist in couchDB.
Here is an example:
module Owner
property :name
end
module Animal
property :type
end
class Cat
include Owner
include Animal
end
This doesn't work. I got this error: "undefined method `property'".
I tried added CouchRest::Model::Embeddable but it won't work for
module either. All the examples I am seeing are extending from
CouchRest::Model::Base. However, I won't be able to use this approach
because Ruby doesn't support multiple inheritance.
I won't be able to change the underlying JSON format. My desired format is {"name":"tom","type":"cat"}.
Any help would be appreciated. Thanks!
According to http://www.couchrest.info/model/embedding.html I think your example would be:
class Owner < CouchRest::Model::Base
include CouchRest::Model::Embeddable
property :name
end
class Animal < CouchRest::Model::Base
include CouchRest::Model::Embeddable
property :type
end
class Cat
property :owner, Owner
property :animal, Animal
end

Resources