I'm trying to call a global method in a file in the lib/ directory from a model. I've already tried with concerns and the problem persists.
The app is developed in Ruby on Rails 5.2.1 and Ruby 2.5.3
# Expense model
class Expense < ApplicationRecord
include Helpers
def self.quantity_this_month
select { |e| year_month(e.date) == year_month(Date.today)
}.count
end
end
# Helper in lib/ directory
module Helpers
def year_month(date)
date.strftime('%Y/%m')
end
end
# in console
Expense.quantity_this_month
Implementing the code of the helper directly in the model method gives the expected result, but right now it shows this error:
undefined method `year_month' for #<Class:0x00007f2f6f0e4b88>
Any ideas?
You need to extend instead of include because the method you want to call it from is a class method.
Also, just a side note, your self.quantity_this_month loads all the Expense records into memory, but it's possible to do the date filtering and counting all within a single SQL query. The below is how it's done in MySQL, this might be slightly different with other databases:
class Expense < ApplicationRecord
extend Helpers # <~~~~~~~ changed to extend
def self.quantity_this_month
where(
"DATE_FORMAT(date, '%Y/%m') = ?",
year_month(Date.today)
).count
end
end
Related
I have the following standard Rails ActiveRecord Foo defined:
# app/models/foo.rb
class Foo < ApplicationRecord
end
And I'm trying to call Foo.find(..) from within a hierarchy that contains a module also named Foo..
# lib/commands/bar.rb
module Commands
module Bar
module Create
class Command
def initialize(params)
...
Foo.find(params[:foo_id]
...
end
end
end
end
end
# lib/commands/foo.rb
module Commands
module Foo
module Create
class Command
...
end
end
end
end
Ruby/Rails is finding Commands::Foo instead of my Foo Model and throwing undefined method 'find' for Commands::Foo:Module.. how can I point at the correct ActiveModel implementation?
The obvious answer is to rename Commands::Foo.. to Commands::Foos.. but I'm curious to know if there's another way :o)
If you want to avoid the clash then you should rename the modules. The existing structure is unwieldy and will present similar problems to all future maintainers.
The best solution that I find in your code is to ensure you call the appropriate module and method via its full path:
2.3.3 :007 > ::Commands::Foo::Create::Command.new
"Commands::Foo::Command reached"
=> #<Commands::Foo::Create::Command:0x007ffa1b05e2f0>
2.3.3 :008 > ::Commands::Bar::Create::Command.new
"Commands::Bar::Command reached"
=> #<Commands::Bar::Create::Command:0x007ffa1b04f110>
You shouldn't try to override or modify internal Rails calls, because then you've modified the framework to fit code, which leads to unpredictable side effects.
You can try to call::Foo in Commands::Foo, it should go with your Foo model
I need some help with my plugin. I want to extend ActiveRecord::Base with a method that initializes another method that can be called in the controller.
It will look like this:
class Article < ActiveRecord::Base
robot_catch :title, :text
...
end
My attempt at extending the ActiveRecord::Base class with robot_catch method looks like following. The function will initialize the specified attributes (in this case :title and :text) in a variable and use class_eval to make the robot? function available for the user to call it in the controller:
module Plugin
module Base
extend ActiveSupport::Concern
module ClassMethods
def robot_catch(*attr)
##robot_params = attr
self.class_eval do
def robot?(params_hash)
# Input is the params hash, and this function
# will check if the some hashed attributes in this hash
# correspond to the attribute values as expected,
# and return true or false.
end
end
end
end
end
end
ActiveRecord::Base.send :include, Plugin::Base
So, in the controller, this could be done:
class ArticlesController < ApplicationController
...
def create
#article = Article.new(params[:article])
if #article.robot? params
# Do not save this in database, but render
# the page as if it would have succeeded
...
end
end
end
My question is whether if I am right that robot_catch is class method. This function is to be called inside a model, as shown above. I wonder if I am extending the ActiveRecord::Base the right way. The robot? function is an instance method without any doubt.
I am using Rails 3.2.22 and I installed this plugin as a gem in another project where I want to use this functionality.
Right now, it only works if I specifically require the gem in the model. However, I want it the functionality to be included as a part of ActiveRecord::Base without requiring it, otherwise I'd have to require it in every model I want to use it, not particularly DRY. Shouldn't the gem be automatically loaded into the project on Rails start-up?
EDIT: Maybe callbacks (http://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html) would be a solution to this problem, but I do not know how to use it. It seems a bit obscure.
First, I would suggest you make sure that none of the many many built in Rails validators meet your needs.
Then if that's the case, what you actually want is a custom validator.
Building a custom validator is not as simple as it might seem, the basic class you'll build will have this structure:
class SpecialValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# Fill this with your validation logic
# Add to record.errors if validation fails
end
end
Then in your model:
class Article < ActiveRecord::Base
validates :title, :text, special: true
end
I would strongly suggest making sure what you want is not already built, chances are it is. Then use resources like this or ruby guides resources to continue going down the custom validator route.
Answer
I found out the solution myself. Bundler will not autoload dependencies from a gemspec that my project uses, so I had to require all third party gems in an engine.rb file in the lib/ directory of my app in order to load the gems. Now everything is working as it should.
Second: the robot_catch method is a class method.
I have trouble thinking of a way on how to shorten my process on titleizing values upon rendering them in my view.
I did some custom getters for the following attributes that I need to titleize. Here's my example.
user.rb
class User < ActiveRecord::Base
def department
read_attribute(:department).titleize
end
def designation
read_attribute(:designation).titleize
end
end
This method works but it seems a hassle when I want to do this to other models as well.
Is there a more efficient way to handle this which can be used by other models? If you'll mention Draper (since I don't seem to find on how to titleize selected attributes), how can I accomplish using this gem? But, I would prefer not using a gem but instead, create a custom one.
Not tested this, but you could use a Concern with added modules to handle it
--
Modularity
I found a gem called modularity which basically allows you to pass parameters to a concern & other modules. This means if you can pass the params you wish to "titleize", you may be able to pull it off like this:
#Gemfile
gem 'modularity', '~> 2.0.1'
#app/models/concerns/titleize.rb
module Titleize
extend ActiveSupport::Concern
as_trait do |*fields|
fields.each do |field|
define_method("#{field}") do
self[field.to_sym] = field.titleize
end
end
end
end
#app/models/your_model.rb
Class YourModel < ActiveRecord::Base
include Titleize[:your, :params]
end
If you want those value always titleized, what you are doing is fine, but I would actually apply the method on the setters, not on the getters, so you only do it once per record instead of at each read:
def department=(s)
write_attribute(:department, s.to_s.titleize) # The to_s is in case you get nil/non-string
end
If this is purely for presentation (ie, you want the not titleized version in the database, then it can be done in a presenter using Draper:
class UserDecorator < Draper::Decorator
delegate_all
def designation
object.designation.titleize
end
end
(or another rails presenter).
Ripping my hair out on this one:
app/models/concerns/soft_delete.rb:
module SoftDelete
extend ActiveSupport::Concern
module ClassMethods
def testing_a_class
pp "Ima class method"
end
end
def testing_an_instance
pp "Ima instance method"
end
end
class User < ActiveRecord::Base
include SoftDelete
end
app/models/user.rb:
class User < ActiveRecord::Base
testing_a_class
end
Now, in the rails console:
x = User.first # I expect "Ima class method" to be printed to the screen
NameError: undefined local variable or method `testing_a_class' for User(no database connection):Class
I don't know where you saw this idea of including a module in the same file where it's defined, but you must not do it (in rails), because of how rails works (auto-lazy-loading).
Rails doesn't load all classes on startup. Instead when you reference a class that doesn't yet exist, rails attempts to guess where it might be located and loads it from there.
x = User.first
Before this line, constant User does not exist (assuming it wasn't referenced before). When trying to resolve this name, rails will look for file user.rb in each of autoload_paths (google it up). It will find one at app/models/user.rb. Next time you reference User, it will just use this constant and will not look it up in the filesystem.
# app/models/user.rb:
class User < ActiveRecord::Base
testing_a_class
end
The definition that was found contains only an invocation of some unknown method (hence the error). Code in your concern file was not loaded and will never be loaded. To fix this, include the concern in the model file.
# app/models/user.rb:
class User < ActiveRecord::Base
include SoftDelete
testing_a_class
end
Now it should work.
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.