Multiple activesupport concerns in mongoid model - ruby-on-rails

I'm not sure I understand how concerns work. I am trying to wrap up some common code into two modules that extend ActiveSupport::Concern, but when I include both, I get a error:
`included': Cannot define multiple 'included' blocks for a Concern
(ActiveSupport::Concern::MultipleIncludedBlocks)
module AppCore
class Student
include Mongoid::Document
include Mongoid::Timestamps
include AppCore::Extensions::Models::TenantScoped
include AppCore::Extensions::Models::UserScoped
end
end
module AppCore::Extensions::Models
module TenantScoped
extend ActiveSupport::Concern
included do
field :tenant_id, type: Integer
belongs_to :tenant, class_name: 'AppCore::Tenant'
association_name = self.to_s.downcase.pluralize
AppCore::Tenant.has_many association_name.to_sym, class_name: self.to_s
end
end
end
module AppCore::Extensions::Models
module UserScoped
extend ActiveSupport::Concern
included do
field :user_id, type: Integer
belongs_to :user, class_name: 'AppCore::User'
end
end
end
Can I only include one Concern at a time? Should I move the two Scoped modules to tenant_scoped and user_scoped to ClassMethods and just make one model extension concern?

Your problem is that you most likely not follow the Rails auto loading conventions regarding folder/file naming that follows the module / class name structure.
See Cannot define multiple 'included' blocks for a Concern (ActiveSupport::Concern::MultipleIncludedBlocks) with cache_classes = true for more info.

I'm not exactly sure what the problem with ActiveSupport::Concern but I'm not a huge fan of its abstraction. I would just use standard ruby to do what you're trying to accomplish and you won't have a problem. Change both of your modules to look like the following
module AppCore::Extensions::Models
module UserScoped
def self.included(klass)
klass.class_eval do
field :user_id, type: Integer
belongs_to :user, class_name: 'AppCore::User'
end
end
end
end

Related

Use of concern in rails 4

I tried to to use concerns in my project. I would like to build a method to get the date in french for every model I have. Here is my code. Currently, I get the error : wrong argument type Class (expected Module) at the line include DateTime in the model.
Here is my file models/concerns/date_time.rb
module DateTime
extend ActiveSupport::Concern
def self.included(base)
base.extend ClassMethods
base.class_eval do
scope :disabled, -> { where(disabled: true) }
end
end
# methods defined here are going to extend the class, not the instance of it
module ClassMethods
def date_string
h = {1=>'Janvier',2=>'Février',3=>'Mars',4=>'Avril',5=>'Mai',6=>'Juin',7=>'Juillet',8=>'Août',9=>'Septembre',10=>'Octobre',11=>'Novembre',12=>'Décembre'}
"#{self.created_at.day}-#{h[self.created_at.month]}-#{self.created_at.year}"
end
end
end
Here is my file models/demands.rb
class Demand < ActiveRecord::Base
include DateTime
belongs_to :skill
belongs_to :project
belongs_to :user
has_many :transactions
validates :project, presence: true
validates :skill, presence: true
validates :title, presence: true
end
Thanks in advance for your help !
Use the Rails built in I18n functionality instead. Doing localization in the model layer is just wrong. Models should only be concerned with data and business logic - not how data (like dates) are presented.
Your immediate issue here is that, because DateTime is a class in the Ruby standard library, Ruby is trying to include that class, not your module. If you rename the module to something unique, say, UsesDateTime, your error should go away.
That said, for this particular method, I agree with max.

Using ActiveRecord:Relation for specific classes, not all relations across ActiveRecord

I'm building a gem in which part of its purpose is to extend associations on a target Class. Although I can easily extend all associations by using something like :
ActiveRecord::Relation.send(:include, MyGem::ActiveRecord::RelationMethods)
This is too broad, and for a Rails App that may use this Gem, I don't want to extend associations for all Classes.
For better granularity, I want to provide the equivalent of :
class User < ActiveRecord::Base
has_many :messages, :extend => MyGem::ActiveRecord::RelationMethods
has_many :comments, :extend => MyGem::ActiveRecord::RelationMethods
end
By using
class User < ActiveRecord::Base
acts_as_my_fancy_gem
has_many :messages
has_many :comments
end
The problem I have is trying to conditionally extend associations within the Gem, when acts_as_my_fancy_gem is added to a class. This is the bare bones of it.
module MyGem
extend ActiveSupport::Concern
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_as_my_fancy_gem
include MyGem::InstanceMethods
end
end
module InstanceMethods
...
end
end
I've looked into reflections, but at this point can find a clear path, and have simply taking stabs in the dark to experiment.
UPDATE:
Currently, I can achieve this with each association by providing a class method like
class User < ActiveRecord::Base
has_many :messages
has_many :comments
fancy_extend :messages
end
module MyGem
extend ActiveSupport::Concern
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_as_my_fancy_gem
include MyGem::InstanceMethods
end
def fancy_extend *associations
class_eval do
associations.each do |association|
reflections[association].options[:extend] = MyGem::ActiveRecord::RelationMethods
end
end
end
end
module InstanceMethods
...
end
end
Adding this approach into the act_as_my_fancy method (which is where I would like to have it) gives me :
# NoMethodError: undefined method `options' for nil:NilClass
Is this rail4? I did not find the :extend option documented. It looks like rails 4 uses blocks to do that nowadays.
It could be as simple as this:
module Fancy
def has_many(name, scope = nil, options = {})
super(name, scope, options) do
def doit
"i did"
end
end
end
end
# in your model
extend Fancy
YourModel.your_relation.doit # => 'i did'

Rails/Ruby Mixin brings unwanted other class methods

Just hoping someone can explain this for me..
I have a Site class that imports a module:
Site
class Site < ActiveRecord::Base
include TrackableChanges
...
In trackable_changes.rb
I have this code..
module TrackableChanges
include ActiveSupport::Callbacks
def self.included(base)
# Initialize module.
base.has_many :change_requests, :as => :model, :dependent => :destroy
#Callbacks
base.before_save :before_save_change_request
base.after_save :after_save_change_request
base.before_destroy :before_destroy_change_request
Facility
end
...
The reference to Facility is really confusing me (I put a trivial reference in here..). Basically in Facility.rb I have this..
class Facility < ActiveRecord::Base
acts_as_citier
acts_as_citier looks a bit like this:
module Citier
def self.included(base)
# When a class includes a module the module’s self.included method will be invoked.
base.send :extend, ClassMethods
end
end
ActiveRecord::Base.send :include, Citier
Now.. Just by referencing Facility in my initial module it is going to the acts_as_citier gem and extending the ActiveRecord of my Site class. I want the acts_as_citier gem for my Facility but NOT for my Site.
Can anyone help stop this include trail bringing in this unwanted reference!?
EDIT
Seems like I can't reference the class Facility at all without it bringing in the ActiveRecord class additions that is defined in the Facility via it's reference to the gem act_as_citier
cities does this...
ActiveRecord::Base.send :include, Citier
class Facility < ActiveRecord::Base
#attr_accessible :name, :type, :facility_type_id, :accessibility_id, :tldc_id, :site_id, :tldc_approved
acts_as_citier if self.to_s == 'Facility' #Protects other classes from accidently getting the AR additions
Adding a condition to the include stops the acts_as_citier gem from extending any class that references 'Facility'.
I'm assuming the include on my Site class > runs through the module > which when it hits a reference to Facility class > runs through the Facility.rb > which runs through acts_as_citier and then hits the extend to active record line. It helped me remembering that every part of a .rb file is an executable.

Rails extending ActiveRecord::Base

I've done some reading about how to extend ActiveRecord:Base class so my models would have some special methods. What is the easy way to extend it (step by step tutorial)?
There are several approaches :
Using ActiveSupport::Concern (Preferred)
Read the ActiveSupport::Concern documentation for more details.
Create a file called active_record_extension.rb in the lib directory.
require 'active_support/concern'
module ActiveRecordExtension
extend ActiveSupport::Concern
# add your instance methods here
def foo
"foo"
end
# add your static(class) methods here
class_methods do
#E.g: Order.top_ten
def top_ten
limit(10)
end
end
end
# include the extension
ActiveRecord::Base.send(:include, ActiveRecordExtension)
Create a file in the config/initializers directory called extensions.rb and add the following line to the file:
require "active_record_extension"
Inheritance (Preferred)
Refer to Toby's answer.
Monkey patching (Should be avoided)
Create a file in the config/initializers directory called active_record_monkey_patch.rb.
class ActiveRecord::Base
#instance method, E.g: Order.new.foo
def foo
"foo"
end
#class method, E.g: Order.top_ten
def self.top_ten
limit(10)
end
end
The famous quote about Regular expressions by Jamie Zawinski can be re-purposed to illustrate the problems associated with monkey-patching.
Some people, when confronted with a problem, think “I know, I'll use
monkey patching.” Now they have two problems.
Monkey patching is easy and quick. But, the time and effort saved is always extracted back
sometime in the future; with compound interest. These days I limit monkey patching to quickly prototype a solution in the rails console.
You can just extend the class and simply use inheritance.
class AbstractModel < ActiveRecord::Base
self.abstract_class = true
end
class Foo < AbstractModel
end
class Bar < AbstractModel
end
You can also use ActiveSupport::Concern and be more Rails core idiomatic like:
module MyExtension
extend ActiveSupport::Concern
def foo
end
module ClassMethods
def bar
end
end
end
ActiveRecord::Base.send(:include, MyExtension)
[Edit] following the comment from #daniel
Then all your models will have the method foo included as an instance method and the methods in ClassMethods included as class methods. E.g. on a FooBar < ActiveRecord::Base you will have: FooBar.bar and FooBar#foo
http://api.rubyonrails.org/classes/ActiveSupport/Concern.html
With Rails 4, the concept of using concerns to modularize and DRY up your models has been in highlights.
Concerns basically allow you to group similar code of a model or across multiple models in a single module and then use this module in the models. Here is a example:
Consider a Article model, a Event model and a Comment Model. A article or A event has many comments. A comment belongs to either article or event.
Traditionally, the models may look like this:
Comment Model:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
Article Model:
class Article < ActiveRecord::Base
has_many :comments, as: :commentable
def find_first_comment
comments.first(created_at DESC)
end
def self.least_commented
#return the article with least number of comments
end
end
Event Model
class Event < ActiveRecord::Base
has_many :comments, as: :commentable
def find_first_comment
comments.first(created_at DESC)
end
def self.least_commented
#returns the event with least number of comments
end
end
As we can notice, there is a significant piece of code common to both Event and Article Model. Using concerns we can extract this common code in a separate module Commentable.
For this create a commentable.rb file in app/model/concerns.
module Commentable
extend ActiveSupport::Concern
included do
has_many :comments, as: :commentable
end
# for the given article/event returns the first comment
def find_first_comment
comments.first(created_at DESC)
end
module ClassMethods
def least_commented
#returns the article/event which has the least number of comments
end
end
end
And Now your models look like this :
Comment Model:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
Article Model:
class Article < ActiveRecord::Base
include Commentable
end
Event Model
class Event < ActiveRecord::Base
include Commentable
end
One point I will like to highlight while using Concerns is that Concerns should be used for 'domain based' grouping rather than 'technical' grouping. For example, a domain grouping is like 'Commentable', 'Taggable' etc. A technical based grouping will be like 'FinderMethods', 'ValidationMethods'.
Here is a link to a post that I found very useful for understanding concerns in Models.
Hope the writeup helps :)
Step 1
module FooExtension
def foo
puts "bar :)"
end
end
ActiveRecord::Base.send :include, FooExtension
Step 2
# Require the above file in an initializer (in config/initializers)
require 'lib/foo_extension.rb'
Step 3
There is no step 3 :)
Rails 5 provides a built-in mechanism for extending ActiveRecord::Base.
This is achieved by providing additional layer:
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
# put your extensions here
end
and all models inherit from that one:
class Post < ApplicationRecord
end
See e.g. this blogpost.
With Rails 5, all models are inherited from ApplicationRecord & it gives nice way to include or extend other extension libraries.
# app/models/concerns/special_methods.rb
module SpecialMethods
extend ActiveSupport::Concern
scope :this_month, -> {
where("date_trunc('month',created_at) = date_trunc('month',now())")
}
def foo
# Code
end
end
Suppose the special methods module needs to be available across all models, include it in application_record.rb file. If we wants to apply this for a particular set of models, then include it in the respective model classes.
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include SpecialMethods
end
# app/models/user.rb
class User < ApplicationRecord
include SpecialMethods
# Code
end
If you want to have the methods defined in the module as class methods, extend the module to ApplicationRecord.
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
extend SpecialMethods
end
Hope it help others !
Just to add to this topic, I spent a while working out how to test such extensions (I went down the ActiveSupport::Concern route.)
Here's how I set up a model for testing my extensions.
describe ModelExtensions do
describe :some_method do
it 'should return the value of foo' do
ActiveRecord::Migration.create_table :test_models do |t|
t.string :foo
end
test_model_class = Class.new(ActiveRecord::Base) do
def self.name
'TestModel'
end
attr_accessible :foo
end
model = test_model_class.new(:foo => 'bar')
model.some_method.should == 'bar'
end
end
end
I have
ActiveRecord::Base.extend Foo::Bar
in an initializer
For a module like below
module Foo
module Bar
end
end

Extend model in plugin with "has_many" using a module

I have a some code in an engine style plugin which includes some models. In my app I want to extend one of these models. I have managed to add both instance and class methods to the model in question by including a module from within an initializer.
However I cannot seem to add associations, callbacks etc. I get a 'method not found' error.
/libs/qwerty/core.rb
module Qwerty
module Core
module Extensions
module User
# Instance Methods Go Here
# Class Methods
module ClassMethods
has_many :hits, :uniq => true # no method found
before_validation_on_create :generate_code # no method found
def something # works!
"something"
end
end
def self.included(base)
base.extend(ClassMethods)
end
end
end
end
end
/initializers/qwerty.rb
require 'qwerty/core/user'
User.send :include, Qwerty::Core::Extensions::User
You should be able to do this. A little more concise IMHO.
module Qwerty::Core::Extensions::User
def self.included(base)
base.class_eval do
has_many :hits, :uniq => true
before_validation_on_create :generate_code
end
end
end
I think this should work
module Qwerty
module Core
module Extensions
module User
# Instance Methods Go Here
# Class Methods
module ClassMethods
def relate
has_many :hits, :uniq => true # no method found
before_validation_on_create :generate_code # no method found
end
def something # works!
"something"
end
end
def self.included(base)
base.extend(ClassMethods).relate
end
end
end
end
end
The old code is wrong cause the validation and the association are called upon module loading, and this module knows nothing about ActiveRecord. That's a general aspect of Ruby, code inside class or module bodies is called directly when loaded. You don't want that. To get around that you can use the above solution.
In Rails 3, this sounds like a good use case for ActiveSupport::Concern:
module Qwerty::Core::Extensions::User
extend ActiveSupport::Concern
included do
has_many :hits, :uniq => true
before_validation_on_create :generate_code
end
end
class User
include Querty::Core::Extensions::User
# ...
end
Here are the ActiveSupport::Concern docs and the most helpful blog article on it I've found.

Resources