In my Rails I have the following models:
A STI sub-class
class Subscription::Discount < Subscription
def self.new_with_url
...
end
end
and another model class (doing completely different things, this is a STI base class)
class Discount < ActiveRecord::Base
end
So in my controller, I uses Subscription::Discount when I create users:
#user.subscription = ::Subscription::Discount.new_with_url()
However it complains: undefined method 'new_with_url' for #<Class:0x007fbb499c6740>
I think Rails is not calling the right class with new_with_url. On top of that I am not sure what #<Class:0x007fbb499c6740> is. So, two questions:
Without renaming any model, how can I reference Subscription::Discount properly?
Why is the error message saying #<Class:0x007fbb499c6740>, I can understand if it is Discount instead of that anonymous class.
EDIT:
Here are all the relevant models:
app/model/discount.rb
app/model/coffee_discount.rb (CoffeeDiscount < Discount)
app/model/subscription.rb
app/model/subscription/discount.rb (Subscription::Discount < Subscription)
The method is named create_with_url but you're calling new_with_url.
Fix the method name.
Related
I have an application I'm building where I need one model to create instances of another model. I want every Car to have 4 tires.
Car model
class Car < ActiveRecord::Base
has_many :tires
after_create :make_tires
def make_tires
4.times { Tire.create(car: self.id) }
end
end
Tire model
class Tire < ActiveRecord::Base
belongs_to :car
end
However, inside of make_tires there is an error that there is no activerecord method for create or new if I try it for Tire. When I inspect Tire it doesn't have those methods.
How can I remedy this?
The error is this: undefined method 'create' for ActiveRecord::AttributeMethods::Serialization::Tire::Module
I have tested two environments: Testing and Development and they both fail for the same error.
It is a name conflict. Sit down and relax while I explain.
Solution with explanation:
In Ruby classes are just instances of class Class (which is a subclass of class Module). Instances of Module (including instances of Class) are quite weird objects, especially weird is their connection with ruby constants. You can create a new class at any point using standard ruby notation:
my_class = Class.new { attr_accessor :a }
instance = my_class.new
instance.a = 3
insatnce.a #=>
instance.class.name #=> nil
Well, our class has no name. It is just an anonymous class. How do classes get their name? By assigning it to a constant (for the first time):
MyClass = my_class
my_class.name #=> 'MyClass'
When you define class using a class keyword:
class MyClass
...
end
You just create a new instance of Class and assign it to a constant. Because of that, Ruby compiler seeing a constant has no idea whether it is a class or a number under it - it has to make a full search for that constant.
The logic behind finding a constant is quite complex and depends on the current nesting. Your case is quite simple (as there is no nesting), so ruby will try to find Tire class inside your class first and when failed it's subclasses and included modules.
Your problem is that your class inherits from ActiveRecord::Base (which is correct), which includes ActiveRecord::AttributeMethods::Serialization module, which defines Tire constant already. Hence, ruby will use this constant instead, as this is the best match for that name in given context.
To fix it, you must tell the compiler not to look within the current class but directly in the "top namespace" (which in ruby is Object. Seriously, try Object.constants) - you can do that using :: in front of your constant, like ::Tire.
Note: even though it works, this issue is a first warning for you that your code starts to smell. You should look after this ActiveRecord::AttributeMethods::Serialization::Tire::Module thingy as it seems you will encounter it more than once in the future.
Other stuff:
You can simplify your method slightly:
def make_tires
4.times { tires.create }
end
At that point you might encounter some error you had initially. If you do, then please find what is going on with that Tire::Module thing. If you don't care about the smell:
has_many :tires, class_name: '::Tire'
I'm not sure what's causing the exception you are seeing but you have a number of issues. First, you need to pass in a car instance instead of the id in make_tires. Like this:
def make_tires
4.times { Tire.create(car: self) }
end
You also need to have attr_accessible :car in the Tire model. Like this:
class Tire < ActiveRecord::Base
belongs_to :car
attr_accessible :car
end
I am using STI in a Rails app and in order to not have to define routes for all subclasses, I put the following in each subclass:
def self.model_name
Mapping.model_name
end
In the above example, Mapping is the parent model name. Example:
class UserMapping < Mapping; end
Having to put this in each subclass is not very DRY, so I'm looking for a way to set that in the parent somehow, so that each class that inherits from the parent automatically has the model name set as the parent model name.
Perhaps there is even a better way to overcome the routing issue that arises from STI unrelated to setting the model_name - I'm open to suggestions!
Thanks in advance!
Put this in your Mapping class:
class Mapping < ActiveRecord::Base
def self.inherited(subclass)
super
def subclass.model_name
superclass.model_name
end
end
end
Afterwards, all child classes of Mapping will also inherit the parent's model_name.
Another option is to override the model_name method in the superclass to return a custom ActiveModel::Name:
class Mapping < ActiveRecord::Base
def self.model_name
ActiveModel::Name.new(base_class)
end
end
By default model_name passes the current class as the first argument to ActiveModel::Name.new, so each sub-class will receive a different name based on their class. ActiveRecord models have a base_class method which we can use instead to get the base of a single-table inheritance hierarchy.
If you cared to, you could also name it something completely different. This might be useful when you're trying to transition a model to a new name:
class Mapping < ActiveRecord::Base
def self.model_name
ActiveModel::Name.new(self, nil, "AnotherMapping")
end
end
More in the docs
I have a standard ActiveRecord model with the following:
class MyModel < ActiveRecord::Base
custom_method :first_field, :second_field
end
At the moment, that custom_method is picked up by a module sent to ActiveRecord::Base. The functionality basically works, but of course, it attaches itself to every model class, not just MyModel. So if I have MyModel and MyOtherModel in the same action, it'll assume MyOtherModel has custom_method :first_field, :second_field as well.
So, my question is: How do I attach a method (eg: def custom_method(*args)) to every class that inherits from ActiveRecord::Base, but not by attaching it to ActiveRecord::Base itself?
Any ideas appreciated.
===
Edit
The custom_method is currently attached to ActiveRecord::Base by the following:
module MyCustomModule
def self.included(base)
base.extend(self)
end
def custom_method(*args)
# Zippity doo dah - code goes here
end
end
ActiveRecord::Base.send(:include, MyCustomModule)
Do you know about descendants?
ActiveRecord::Base.descendants
You have to be sure to touch the models before calling it.
See excellent discussion here:
Is there a way to get a collection of all the Models in your Rails app?
I concur with the commentors above that you may want to consider adding your methods to the meta class, or an intermediary class, or a Module mixin.
Not sure on my Ruby syntax here.
I want to define a method that I can call like this: client.invoices.average_turnaround. So my average_turnaround method needs to work with a collection of ActiveRecord objects.
Here's my code thus far:
class Invoice < ActiveRecord::Base
...
def self.average_turnaround
return self.map(&:turnaround).inject(:+) / self.count
end
end
So I'm trying to find the sum of the turnaround times for each invoice, then divide it by the total number of invoices.
Ruby is complaining that there is no map method defined for Class. I was expecting self to be an Array.
How do I write a method that works on a collection of Invoices and uses the map function? Where am I going wrong?
If you want to use map within the class method as opposed to through an association extension. For example if it would be useful to call Invoice.average_turnaround directly or Invoice.where(x: y).average_turnaround. Place all. in front of map.
class Invoice < ActiveRecord::Base
...
def self.average_turnaround
all.map(&:turnaround).inject(:+) / all.count
end
end
Use average_turnaround using any collection.
You defined a class method, which is called on the class itself. What you need is an association extension. The method should be defined on your client model like this:
class Client < ActiveRecord::Base
has_many :invoices do
def average_turnaround
return map(&:turnaround).inject(:+) / count
end
end
Here's a Ruby OO head scratcher for ya, brought about by this Rails scenario:
class Product < ActiveRecord::Base
has_many(:prices)
# define private helper methods
end
module PrintProduct
attr_accessor(:isbn)
# override methods in ActiveRecord::Base
end
class Book < Product
include PrintProduct
end
Product is the base class of all products. Books are kept in the products table via STI. The PrintProduct module brings some common behavior and state to descendants of Product. Book is used inside fields_for blocks in views. This works for me, but I found some odd behavior:
After form submission, inside my controller, if I call a method on a book that is defined in PrintProduct, and that method calls a helper method defined in Product, which in turn calls the prices method defined by has_many, I'll get an error complaining that Book#prices is not found.
Why is that? Book is a direct descendant of Product!
More interesting is the following..
As I developed this hierarchy PrintProduct started to become more of an abstract ActiveRecord::Base, so I thought it prudent to redefine everything as such:
class Product < ActiveRecord::Base
end
class PrintProduct < Product
end
class Book < PrintProduct
end
All method definitions, etc. are the same. In this case, however, my web form won't load because the attributes defined by attr_accessor (which are "virtual attributes" referenced by the form but not persisted in the DB) aren't found. I'll get an error saying that there is no method Book#isbn. Why is that?? I can't see a reason why the attr_accessor attributes are not found inside my form's fields_for block when PrintProduct is a class, but they are found when PrintProduct is a Module.
Any insight would be appreciated. I'm dying to know why these errors are occurring!
You might have better luck delaying the attr_accessor call in PrintProduct until mixin-time:
module PrintProduct
def self.included(base)
base.attr_accessor :isbn
end
# other instance methods here
end
The problem is likely something to do with timing of the attr_accessor call and how that applies to modules mixed in. I'm not certain that the timing is defined by the Ruby spec, so it might vary betweeen implementations or versions.