With Rails concerns I can give my model class methods and instance methods through modules by including them. No blog entry or thread that I've found mentions how I can include variables in my model though.
Specifically I would like to give my including model a class instance variable #question, but I don't know where to put the declaration in the module so it is applied. I would also like the class instance variable to be overridden if the model itself declares that variable.
Does the ActiveSupport::Concern module actually care about variables at all?
module ContentAttribute
extend ActiveSupport::Concern
def foo
p "hi"
end
module ClassMethods
# #question = "I am a generic question." [doesn't work]
def bar
p "yo"
end
end
end
class Video < ActiveRecord::Base
include ContentAttribute
# #question = "Specific question"; [should override the generic question]
end
module ContentAttribute
extend ActiveSupport::Concern
included do
self.question = "I am a generic question."
end
module ClassMethods
attr_accessor :question
end
end
Then, in video...
class Video < ActiveRecord::Base
include ContentAttribute
self.question = "Specific question"
end
Related
What is a convention for reusing methods between presenters?
For example, say an app has the following presenters
class UserPresenter < BasePresenter
end
class AdminPresenter < BasePresenter
end
class EventPresenter < BasePresenter
end
User and Admin both have avatars. What is the correct way to share an avatar method between the User and Admin presenter?
One solution might be inheriting from an AvatarPresenter
class UserPresenter < AvatarPresenter
end
class AdminPresenter < AvatarPresenter
end
class EventPresenter < BasePresenter
end
class AvatarPresenter < BasePresenter
end
Which works OK in this simple example. But what if things become more complex in the future (e.g., an additional method shared between Admin and Event).
I suppose I'm looking to share Concerns between Presenters. Is this a conventional approach, and if so what would a template implementation look like? All my attempts are raising method not found errors.
What you are looking for is traits. In Ruby this takes the form of module mixins.
module Avatar
def self.included(base)
base.extend ClassMethods
base.class_eval do
# in this block you are operating on the singleton class
# where the module is included
end
end
def an_instance_method_from_avatar
end
module ClassMethods
def a_class_method_from_avatar
end
end
end
class UserPresenter
include Avatar
end
class AdminPresenter
include Avatar
end
This lets us create reusable components that can composed in many different ways. ActiveSupport::Concern takes the pattern above and simplifies it:
module Avatar
# modules can be extended by other modules
extend ActiveSupport::Concern
included do
# in this block you are operating on the singleton class
# where the module is included
end
class_methods do
def a_class_method_from_avatar
end
end
def an_instance_method_from_avatar
end
end
Inheritance (class based) on the other hand should only really be used if an object is a true subtype of its parent. While you could argue that an AdminPresenter is a presenter with an avatar this would lead to a really convoluted class diagram down the road if you need to add other functionality.
Is there a way to use arguments when including a ruby module? I have a module Assetable which is included across many classes. I want to be able to generate attr_accessor's on the fly.
module Assetable
extend ActiveSupport::Concern
included do
(argument).times do |i|
attr_accessor "asset_#{i}".to_sym
attr_accessible "asset_#{i}".to_sym
end
end
end
There is a trick: making a class that's inheriting from a module so that you could pass any arguments to the module like class.
class Assetable < Module
def initialize(num)
#num = num
end
def included(base)
num = #num
base.class_eval do
num.times do |i|
attr_accessor "asset_#{i}"
end
end
end
end
class A
include Assetable.new(3)
end
a = A.new
a.asset_0 = 123
a.asset_0 # => 123
The details are blogged at http://kinopyo.com/en/blog/ruby-include-module-with-arguments, hope you'll find it useful.
There is no way of passing arguments when including the module. The best next thing would be to define a class method that lets you create what you need afterwards:
module Assetable
extend ActiveSupport::Concern
module ClassMethods
def total_assets(number)
number.times do |i|
attr_accessor "asset_#{i}"
attr_accessible "asset_#{i}"
end
end
end
end
class C
include Assetable
total_assets 3
end
o = C.new
o.asset_2 = "Some value."
o.asset_2 #=> "Some value."
Also be careful when overriding the included method within a concern because it's also used by ActiveSupport::Concern. You should call super within the overriden method in order to ensure proper initialization.
You can generate and include an anonymous module without polluting global namespaces:
module Assetable
def self.[](argument)
Module.new do
extend ActiveSupport::Concern
included do
(argument).times do |i|
attr_accessor :"asset_#{i}"
attr_accessible :"asset_#{i}"
end
end
end
end
end
class Foo
include Assetable[5]
end
You can't pass arguments to a module. In fact, you can't pass arguments to anything except a message send.
So, you have to use a message send:
module Kernel
private def Assetable(num)
#__assetable_cache__ ||= []
#__assetable_cache__[num] ||= Module.new do
num.times do |i|
attr_accessor :"asset_#{i}"
attr_accessible :"asset_#{i}"
end
end
end
end
class Foo
include Assetable 3
end
Note: I didn't see why you would need ActiveSupport::Concern here at all, but it's easy to add back in.
I have a model with the following two methods which are required in another model so I thought I'd try sharing them via a concern instead of duplicating the code.
class Region < ActiveRecord::Base
def ancestors
Region.where("lft < ? AND ? < rgt", lft, rgt)
end
def parent
self.ancestors.order("lft").last
end
end
I have created a file in app/models/concerns/sets.rb and my new model reads:
class Region < ActiveRecord::Base
include Sets
end
sets.rb is:
module Sets
extend ActiveSupport::Concern
def ancestors
Region.where("lft < ? AND ? < rgt", lft, rgt)
end
def parent
self.ancestors.order("lft").last
end
module ClassMethods
end
end
Question:
How do I share a method between models when the method references the model such as "Region.where..."
Either by referencing the class of the including model (but you need to wrap the instance methods in an included block):
included do
def ancestors
self.class.where(...) # "self" refers to the including instance
end
end
or (better IMO) by just declaring the method as a class method, in which case you can leave the class itself out altogether:
module ClassMethods
def ancestors
where(...)
end
end
I have defined a module to extend ActiveRecord.
In my case I have to generate instance methods with the symbols given as arguments to the compound_datetime class method. It works when class_eval is called outside the each block but not inside it; in the latter case I get an undefined method error.
Does anyone know what I am doing wrong?
module DateTimeComposer
mattr_accessor :attrs
##attrs = []
module ActiveRecordExtensions
module ClassMethods
def compound_datetime(*attrs)
DateTimeComposer::attrs = attrs
include ActiveRecordExtensions::InstanceMethods
end
end
module InstanceMethods
def datetime_compounds
DateTimeComposer::attrs
end
def self.define_compounds(attrs)
attrs.each do |attr|
class_eval <<-METHODS
def #{attr.to_s}_to()
puts 'tes'
end
METHODS
end
end
define_compounds(DateTimeComposer::attrs)
end
end
end
class Account < ActiveRecord::Base
compound_datetime :sales_at, :published_at
end
When I try to access the method:
Account.new.sales_at_to
I get a MethodError: undefined method sales_at_to for #<Account:0x007fd7910235a8>.
You are calling define_compounds(DateTimeComposer::attrs) at the end of the InstanceMethods module definition. At that point in the code, attrs is still an empty array, and self is the InstanceMethods module.
This means no methods will be defined, and even if they were, they would be bound to InstanceMethods's metaclass, making them class methods of that module, not instance methods of your Account class.
This happens because method calls inside the InstanceMethods module definition are evaluated as they are seen by the ruby interpreter, not when you call include ActiveRecordExtensions::InstanceMethods. An implication of this is that it is possible to run arbitrary code in the most unusual of places, such as within a class definition.
To solve the problem, you could use the included callback provided by ruby, which is called whenever a module is included in another:
module InstanceMethods
# mod is the Class or Module that included this module.
def included(mod)
DateTimeComposer::attrs.each do |attr|
mod.instance_eval <<-METHODS
def #{attr.to_s}_to
puts 'tes'
end
METHODS
end
end
end
As an additional suggestion, you should be able to achieve the same result by simply defining the methods when compound_datetime is called, thus eliminating the dependence on the attrs global class variable.
However, if you must have access to the fields which were declared as compound datetime, you should use class instance variables, which are unique to each class and not shared on the hierarchy:
module ClassMethods
def compound_datetime(*attrs)
#datetime_compounds = attrs
attrs.each do |attr|
instance_eval <<-METHODS
def #{attr.to_s}_to
puts 'tes'
end
METHODS
end
end
def datetime_compounds; #datetime_compounds; end;
end
class Account < ActiveRecord::Base
compound_datetime :sales_at, :published_at
end
class AnotherModel < ActiveRecord::Base
compound_datetime :attr1, :attr2
end
Account.datetime_compounds
=> [ :sales_at, :published_at ]
AnotherModel.datetime_compounds
=> [ :attr1, :attr2 ]
I need some help with virtual attributes. This code works fine but how do I use it inside a plugin. The goal is to add this methods to all classes that uses the plugin.
class Article < ActiveRecord::Base
attr_accessor :title, :permalink
def title
if #title
#title
elsif self.page
self.page.title
else
""
end
end
def permalink
if #permalink
#permalink
elsif self.page
self.page.permalink
else
""
end
end
end
Thanks
You can run the plugin generator to get started.
script/generate plugin acts_as_page
You can then add a module which defines acts_as_page and extends it into all models.
# in plugins/acts_as_page/lib/acts_as_page.rb
module ActsAsPage
def acts_as_page
# ...
end
end
# in plugins/acts_as_page/init.rb
class ActiveRecord::Base
extend ActsAsPage
end
This way the acts_as_page method is available as a class method to all models and you can define any behavior into there. You could do something like this...
module ActsAsPage
def acts_as_page
attr_writer :title, :permalink
include Behavior
end
module Behavior
def title
# ...
end
def permalink
# ...
end
end
end
And then when you call acts_as_page in the model...
class Article < ActiveRecord::Base
acts_as_page
end
It will define the attributes and add the methods. If you need things to be a bit more dynamic (such as if you want the acts_as_page method to take arguments which changes the behavior) try out the solution I present in this Railscasts episode.
It appears that you want a Module for this
# my_methods.rb
module MyMethods
def my_method_a
"Hello"
end
end
The you want to include it into the classes you want to use it for.
class MyClass < ActiveRecord::Base
include MyMethods
end
> m = MyClass.new
> m.my_method_a
=> "Hello!"
Take a look here for more information on mixing in modules. You can put the module wherever in a plugin if you like, just ensure its named correctly so Rails can find it.
Create a module structure like YourPlugin::InstanceMethods and include it this module like this:
module YourPlugin
module InstanceMethods
# your methods
end
end
ActiveRecord::Base.__send__(:include, YourPlugin::InstanceMethods)
You have to use __send__ to make your code Ruby 1.9 compatible. The __send__ line is usually placed at the init.rb file on your plugin root directory.