I am in need of adding set of generic methods to an existing model. I've found this tutorial:
http://chris-schmitz.com/extending-activemodel-via-activesupportconcern/
which in my opinion is what I am aiming at (I want to have a module which will be added to the model to add some methods to it - a sort of mixin).
Now even if I do plain copy paste from the tutorial I am struck at the following error (with no further explanation):
undefined method `key?' for nil:NilClass
Here is what my model looks like:
class Folder < ActiveRecord::Base
attr_accessible :name, :parent_id
has_default
validates :name, presence: true
end
The moment I remove has_default everything goes back to normal
Check agains your code...
The modules structure may look like this (taken from one of my projects that definitely works):
# lib/taggable.rb
require 'active_support/concern'
module Taggable
extend ActiveSupport::Concern
module ClassMethods
def taggable
include TaggableMethods # includes the instance methods specified in the TaggableMethods module
# class methods, validations and other class stuff...
end
end
module TaggableMethods
# instance methods...
end
end
What is missing is that you should tell Rails to load the module from the lib directory:
# config/application.rb
module AppName
class Application < Rails::Application
# Custom directories with classes and modules you want to be autoloadable.
# config.autoload_paths += %W(#{config.root}/extras)
config.autoload_paths += %W(#{config.root}/lib)
# rest ommited...
Now the modules should be included.
# model.rb
class Model
taggable
end
This is how basic plugins works. The author of the tutorial mentioned in your question writes a plugin specific only for models that inherit from ActiveRecord::Base for he is using its specific methods (e.g. update_column).
If your modules does no rely on ActiveRecord methods, it is not necessary to extend it (the module may also be used by Mongoid models). But this is definitely NOT the right way:
class ActiveRecord::Base
include HasDefault
end
If you really need to extend ActiveRecord, do it this way:
ActiveRecord::Base.extend ModuleName
Of course there are lots of other ways to write plugins depending on your needs, take various rails gems as a good inspiration.
Related
I am looking to separate concerns for some subset of function specific to a model.
I have referenced here and followed this pattern
module ModelName::ConcernName
extend ActiveSupport::Concern
included do
# class macros
end
# instance methods
def some_instance_method
end
module ClassMethods
# class methods here, self included
end
end
However, when I try to start the server it would result in the following error
Circular dependency detected while autoloading constant ModelName::ConcernName
I am wondering what is the best way to do concerns for some subset functions of a model.
Edit
Providing the model code:
path: app/models/rent.rb
Now I have a lot of checking logic in my model
class Rent < ActiveRecord::Base
def pricing_ready?
# check if pricing is ready
end
def photos_ready?
# check if photo is ready
end
def availability_ready?
# check if availability setting is ready
end
def features_ready?
# check if features are set
end
end
I want to separate it in concern
class Rent < ActiveRecord::Base
include Rent::Readiness
end
And organise the concern by namespace
path: app/models/concerns/rent/readiness.rb
module Rent::Readiness
extend ActiveSupport::Concern
included do
# class macros
end
# instance methods
def pricing_ready?
# check if pricing is ready
end
...
module ClassMethods
# class methods here, self included
end
end
Now I got it working if I just do class RentReadiness with the path in app/models/concerns/rent_readiness.rb
You can scope it to Rents and place to concerns/rents/readiness.rb:
module Rents
module Readiness
extend ActiveSupport::Concern
included do
# class macros
end
end
end
And in model:
class Rent < ActiveRecord::Base
include Rents::Readiness
end
You can make it work by just moving the folder with model-specific concerns from concerns to models. So, you would have:
models/
rent.rb
rent/
readiness.rb
I like this convention of using the model as the namespace for its concerns because it lets you remove some redundancy from code:
Since you are defining the concern within the model class, in the model you can write include Readiness instead of include Rent::Readiness
When defining the concern, you can use module Rent::Readiness instead of
class Rent < ApplicationRecord
module Readiness
...
Which would be another way of fixing the circular dependency problem you mentioned in your question.
Rails uses activesupport to load classes and modules as they are defined by inferring the file path based on the class or module name, this is done as the Ruby parser loads your files and come across a new constant that has not been loaded yet. In your case, the Rent model is parsed up to the Rent::Readlines reference, at which point activesupport goes off to look for the rent/readlines.rb code file that matches the name. This file is then then parsed by ruby, but on the first line, The still unloaded Rent class is referenced, which triggers activesupport to go off and look for the code file that matches the name.
We have a concern we want to use as a mixin for our User classes. This concern is found in our separate rails engine that we use for multiple products.
Everything in this engine, we keep in the same module, which we will call MyEngine.
module MyEngine
module EngineUser
extend ActiveSupport::Concern
end
end
And we are tring to include it like any other concern in our Rails Application:
class User < ActiveRecord::Base
include MyEngine::EngineUser
# ...
end
This causes an error where it says: (formatted some for readability)
/Users/foo/.rvm/gems/ruby-2.1.5/gems/activesupport-4.2.4/lib/active_support/dependencies.rb:495:in `load_missing_constant':
Unable to autoload constant EngineUser,
expected
/Users/foo/Perforce/engine_folder/app/models/concerns/engine_user.rb
to define it
Which..... is the right file... <.<
If I remove the module MyEngine around the class, and form the include in User, it works just fine.
I know rails does so autoloading behind the scenes, but why isn't this working? It knows that file has the class... if I move it to engine/app/concerns it says it cant find it there. So frustrating.
This should solve your problem. I had the same issue recently..
Take a look at the additional module, Concerns, I've added.
module MyEngine
module Concerns
module EngineUser
extend ActiveSupport::Concern
end
end
end
# lib/my_engine/engine.rb
module MyEngine
class Engine < ::Rails::Engine
config.autoload_paths += %W(#{MyEngine::Engine.root}/app/models/my_engine/concerns/engine_user.rb)
isolate_namespace MyEngine
end
end
# app/models/user.rb
class User < ActiveRecord::Base
include MyEngine::Concerns::EngineUser
# ...
end
Here is the RailsGuide post that led me to the answer.
So the answer from Justin Licata is probably the "correct" answer, but the solution that I ended up going with was as follows:
in engine_folder/lib/my_engine.rb, which is what is included as part of loading the engine, I just used the line:
Dir[File.dirname(__FILE__) + "/my_engine/concerns/**/*.rb"].each { |file| require file[0..-4]}
It certainly has some smell to it, I admit, but it lets us follow our own conventions and structure, and allows us to add folder hierarchy as needed without worrying about rails autoloading issues.
Thanks for the answers!
I am doing Rails app and i see that i could refactor my code. Just can't find how. I have many models like Company, News, Profile and many more which use Image upload. So in every class i must always copy-paste 30 rows of methods which implement always the same logic - upload_image, get_image_name, delete_image. How is possible to do, that my Model class would automatically have the methods from somewhere else? I would like to just put to the Model - load 'GlobalMethods', or even somehow include the methds in activerecord:base to just have them for every class and i would use whenever i want. And from controller i would just leave as it is. For example - News.upload_image and it would do this same as this logic would be in original model.
Please explain with example, because i have readed more and didn't understand or it even possible.
At this moment i did:
#models/concerns/uploading.rb
module Uploading
extend ActiveSupport::Concern
included do
def do_upload
puts 'aaaaaaaaaaaaaaa'
end
end
end
and model:
class Company < ActiveRecord::Base
include Uploading
end
and i get this error:
uninitialized constant Company::Uploading
My rails version: '4.2.5'
I did server restarts after every try
My application.rb file looks:
require File.expand_path('../boot', __FILE__)
require 'rails/all'
Bundler.require(*Rails.groups)
module Vca
class Application < Rails::Application
config.active_record.raise_in_transactional_callbacks = true
config.autoload_paths += %W(
#{config.root}/app/models/concerns/uploading.rb
)
end
end
Still the same
NameError in CompaniesController#index
uninitialized constant Company::Uploading
class Company < ActiveRecord::Base
include Uploading # <-- error
end
I have gem 'Spring'. I turned off server, in terminal i runned "spring stop" and started server. But it didn't solve this.
Take a look at ActiveSupport::Concern
For example
app/models/concerns/my_concern.rb
module MyConcern
extend ActiveSupport::Concern
included do
# common stuff here
end
class_methods do
# define class methods here
end
end
app/models/my_model.rb
class MyModel < ActiveRecord::Base
include MyConcern
# ...
end
By the way, you can refer to the model class with self in the included block and out of any instance methods, and inside the methods within the class_methods block.
You'll notice that your standard rails project includes a folder called concerns under your models folder.
Create a module in that folder, image_handling.rb
module ImageHandling
extend ActiveSupport::Concern
included do
# ...(include all your common methods here)
end
end
In your Company, News, Profile models call the concern...
class Company < ActiveReecord::Base
include ImageHandling
...
end
I have created a simple railtie, adding a bunch of stuff to ActiveRecord:
0 module Searchable
1 class Railtie < Rails::Railtie
2 initializer 'searchable.model_additions' do
3 ActiveSupport.on_load :active_record do
4 extend ModelAdditions
5 end
6 end
7 end
8 end
I require this file (in /lib) by adding the following line to config/environment.rb before the application is called:
require 'searchable'
This works great with my application and there are no major problems.
I have however encountered a problem with rake db:seed.
In my seeds.rb file, I read data in from a csv and populate the database. The problem I am having is that the additions I made to ActiveRecord don't get loaded, and seeds fails with a method_missing error. I am not calling these methods, but I assume that since seeds.rb loads the models, it tries to call some of the methods and that's why it fails.
Can anyone tell me a better place to put the require so that it will be included every time ActiveRecord is loaded (not just when the full application is loaded)? I would prefer to keep the code outside of my models, as it is code shared between most of my models and I want to keep them clean and DRY.
Putting the extend there just adds it to ActiveRecord::Base.
When a model class is referenced, via Rails 3.1 autoloading/constant lookup, it will load the class. At that point, it is pure Ruby (nothing magic) as to what happens, basically. So I think you have at least a few options. The "bad" option that kind of does what you want it to hook into dependency loading. Maybe something like:
module ActiveSupport
module Dependencies
alias_method(:load_missing_constant_renamed_my_app_name_here, :load_missing_constant)
undef_method(:load_missing_constant)
def load_missing_constant(from_mod, const_name)
# your include here if const_name = 'ModelName'
# perhaps you could list the app/models directory, put that in an Array, and do some_array.include?(const_name)
load_missing_constant_renamed_my_app_name_here(from_mod, const_name)
end
end
end
Another way to do it would be to use a Railtie like you were doing and add a class method to ActiveRecord::Base that then includes stuff, like:
module MyModule
class Railtie < Rails::Railtie
initializer "my_name.active_record" do
ActiveSupport.on_load(:active_record) do
# ActiveRecord::Base gets new behavior
include ::MyModule::Something # where you add behavior. consider using an ActiveSupport::Concern
end
end
end
end
If using an ActiveSupport::Concern:
module MyModule
module Something
extend ActiveSupport::Concern
included do
# this area is basically for anything other than class and instance methods
# add class_attribute's, etc.
end
module ClassMethods
# class method definitions go here
def include_some_goodness_in_the_model
# include or extend a module
end
end
# instance method definitions go here
end
end
Then in each model:
class MyModel < ActiveRecord::Base
include_some_goodness_in_the_model
#...
end
However, that isn't much better than just doing an include in each model, which is what I'd recommend.
I wrote an upsert method for one of my models. I would like all my models to have this upsert method. It seemed to me that the logical solution was to define a model that inherits from ActiveRecord::Base and then have all my other models inherit from that. But if I do that, Rails complains that the new model I created doesn't have a table to go with it, which is true, but I don't care.
Since the way I tried is apparently not the right way to do it, what's the right way to do it?
You can extend ActiveRecord with a module. you only do it in one place and it will be accessible for all models that inherits from ActiveRecord.
module YourModule
def self.included(recipient)
recipient.extend(ModelClassMethods)
recipient.class_eval do
include ModelInstanceMethods
end
end # #included directives
# Class Methods
module ModelClassMethods
# A method accessible on model classes
def whatever
end
end
# Instance Methods
module ModelInstanceMethods
#A method accessible on model instances
def another_one
end
end
end
#This is where your module is being included into ActiveRecord
if Object.const_defined?("ActiveRecord")
ActiveRecord::Base.send(:include, YourModule)
end
There are two ways to do this.
1) To have a parent model, but not need to create a table for it (i.e. an abstract class) you should set
class YourAbstractClass < ActiveRecord::Base
self.abstract_class = true
# rest of class code
end
2) Put the method in a module, that you include from all your models that need it (as in #Mark's answer)
You can move that method to a module and include that module in all the models that require that method.
Like I have this Utils module in lib folder of my app
module Utils
...
def to_name(ref)
ref.gsub('_', ' ').split.collect { |w| w.capitalize }.join(' ')
end
...
end
Then in my model, I say
class MyModel < AR::Base
include Utils
...
end
Probably, if you are using Rails 3, you should load the files in the lib folder by configuring your application.rb
config.autoload_paths += %W(#{config.root}/lib)