Ruby - How to access module's methods? - ruby-on-rails

I'm installing a forum using the Forem gem. There's an option that allows avatar personalization, since it's possible to login with Facebook. You just specify your method in the User model and that's it.
# Forem initializer
Forem.avatar_user_method = 'forem_avatar'
# User model
def forem_avatar
unless self.user_pic.empty?
self.user_pic
end
end
But I want a fallback on Gravatar for normal, non-facebook accounts. I've found the method on Forem and in theory, I need to call the avatar_url method:
# User model
def forem_avatar
unless self.user_pic.empty?
self.user_pic
else
Forem::PostsHelper.avatar_url self.email
end
end
However, Forem isn't an instance, but a module and I can't call it nor create a new instance. The easy way is to copy the lines of that method, but that's not the point. Is there a way to do it?
Thanks
Update
Both answers are correct, but when I call the method either way, there's this undefined local variable or method 'request' error, which is the last line of the original avatar_url.
Is there a way to globalize that object like in PHP? Do I have to manually pass it that argument?

perhaps reopen the module like this:
module Forem
module PostsHelper
module_function :avatar_url
end
end
then call Forem::PostsHelper.avatar_url
if avatar_url call other module methods, you'll have to "open" them too via module_function
or just include Forem::PostsHelper in your class and use avatar_url directly, without Forem::PostsHelper namespace

If you want to be able to use those methods in the user class, include them and use
class User < ActiveRecord::Base
include Forem::PostsHelper
def forem_avatar
return user_pic if user_pic.present?
avatar_url email
end
end

Another way would be to set the Forem.avatar_user_method dynamically since the Forem code checks it it exists before using it and defaults to avatar_url if it does not.
class User < ActiveRecord::Base
# This is run after both User.find and User.new
after_initialize :set_avatar_user_method
# Only set avatar_user_method when pic is present
def set_avatar_user_method
unless self.user_pic.empty?
Forem.avatar_user_method = 'forem_avatar'
end
end
def forem_avatar
self.user_pic
end
end
This way you dont pollute your model with unnecessary methods from Forem and don't monkey patch Forem itself.

Related

Is it possible to call a previously defined method from within a method of the same name?

Is it possible to overwrite a method and still fallback to the original method (given no superclass is involved)?
def User
def method
# do some original stuff
end
def method
# do some new stuff
call_the_original :method
end
end
Hopefully, my specific example will make my meaning more clear.
Using activestorage has_one_attached :avatar in a User model adds a setter method. I want to do some stuff when this setter is called, but I still want the original method to run.
class User
has_one_attached :avatar
# According to the source (see references) this mixes in the following setter method
def avatar=(attachable)
# do activestorage stuff
end
# I want to add some custom functions to this, before still running "do activestorage
# stuff". I could copy, paste and edit the entire function. But out of interest,
# I wondered if a more elegant solution exists.
def avatar=(attachable)
# do my stuff
super(attachable)
end
end
super obviously does not work because User is not inheriting from anything in which avatar=() is defined.
I could create e.g. MasterUser class containing has_one_attached and from which User inherits, but this seems overkill for just this particular case.
I could submit to a custom_avatar_method=(attachable) which calls avatar=(attachable).
But with this question what I'm really interested in is whether there a way to call a previously defined method from a method of the same name?
References:
#has_one_attached source
You can make use of alias_method to access the previous definition here:
class User
def avatar=(attachable)
# do activestorage stuff
end
alias_method :original_avatar=, :avatar=
def avatar=(attachable)
# do my stuff
self.original_avatar=(attachable)
end
end
Another option is saving the old method inside a variable before defining the new method with the same name. Then call the variable from inside the newly defined method:
class User
def avatar=(attachable)
# do activestorage stuff
end
instance_method(:avatar=).tap do |avatar_eq|
define_method(:avatar=) do |attachable|
# do my stuff
avatar_eq.bind(self).call(attachable)
end
end
end
In the above example define_method(:avatar=) has to be used, since a regular def avatar= wont let you access the avatar_eq variable.
The code is somewhat more complicated than JagdeepSinghs answer, but leaves the class less cluttered with methods. The old method is no longer defined and thus can no longer be called by itself.
References:
Module#instance_method to get the previously defined method
Object#tap to namespace a variable to a small portion of the class definition
Module#define_method to define the new method with the same name
UnboundMethod#bind to bind the unbound method to the current User instance
Method#call to call the bound previously defined method

Why do functions from my Rails plugin not work without specifically requiring?

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.

Creating custom reusable method in rails 4

Guys today I'm trying to create global method for all my project models in rails 4
I created something like that under this path lib/query.rb
module Query
def custom my_query
self.where(my_query)
end
end
then added this code in this file lib/application.rb to allow rails to load the files under this path
# Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths += %W(#{config.root}/lib)
then included my method in my model by using this command
include Query
now should every thing ready to use my custom method , but when I tried to call my method in the controller like that
def index
#users= Users.custom(params[:query])
end
I got the error
undefined method `custom'
what I should do now ??
why i got this error ??
I think you should use concern for your module. Add your file in app/models/concerns.
# app/models/concerns/query.rb
module Query
extend ActiveSupport::Concern
included do
#you can use a scope
scope :my_query, ->(just_a_param){ .... }
end
module ClassMethods
#or a method
def self.another_query
where(....)
end
end
end
Of course you need to include the module in your model. As concern erd default in rails, you no longer need to change config autoload paths.
As a class method, you'll need the "self."
def self.custom my_query
self.where(my_query)
end
EDIT: If you want this in all ActiveRecord models, you can add it as an initializer
#config/initializers/active_record_extensions.rb
class ActiveRecord::Base
def self.custom my_query
self.where(my_query)
end
end
If you just want this on a single class, a concern would work.
In your example, there is no reference given between your class Users and your method custom. First: if Users refers to a Ruby on Rails class it is probably called User (see also comment of japed). So change the call. Next, your User class must inherited from ActiveRecord else it would not be aware of the existence of 'where'. For details check your app/models/user.rb
Then Swards' suggestion should work for you. Stop your application and restart. Now it should work.
Guys I found the true way to make it
First my impropriety was the include that I set in the model
It should be extend Query
then it will work well
so the true code will be
create your method file under this path lib/query.rb
then set this code in it
module Query
def custom my_query
self.where(my_query)
end
end
then added this code in this file lib/application.rb
# Custom directories with classes and modules you want to be autoloadable.
config.autoload_paths += %W(#{config.root}/lib)
then extend the method in the model by using this command
extend Query
and in your controller query you can use the method like that
def index
#users= Users.custom(params[:query])
end
This is my solution, not exactly the 'Rails way', but using some sort of decorator pattern:
#user = CustomQuery.find_for(User.find(params[:search])).perform!
class CustomQuery
attr_reader :params, :klass
def initialize(klass)
#params = params
#klass = klass
end
def self.find_for(params)
CustomQuery.new(params)
find_model_for(params.tap {})
end
def perform!
return params unless params.nil?
klass.all
end
def find_model_for(klass)
#klass = klass
end
end
While I'm not sure about the process to create a global method, I can tell that your Ruby code is not valid:
def custom my_query
self.where(my_query)
end
It would need to be:
def custom (my_query)
self.where(my_query)
end

Ruby on Rails, calling a very big method from a model

I have a very big function in my model and I want to store it somewhere else in order to keep my model dry. I read that storing methods in ApplicationHelper and then calling them from a model is a bad idea. What is a good idea then?
I want to have a separate file with my big methods and call them from a model.
You can create a "plain old ruby object (PORO)" to do your work for you. let's say you had a method that calculates the amount overdue for a user.
So, you can create app/services/calculates_overages.rb
class CalculatesOverages
def initialize(user)
#user = user
end
def calculate
# your method goes here
end
end
Then, you can:
class User < ActiveRecord::Base
def overage_amount
CaluclatesOverage.new(self).calculate
end
end
Or, in a controller you could:
def show
#amount = CaluclatesOverage.new(current_user).calculate
end
The app/services directory could also be app/models, or the lib directory. There's no set convention for this (yet).
Use a Concern. https://gist.github.com/1014971
It's simple. In app/models/concerns create a file your_functionality.rb as follows:
module YourFunctionality
extend ActiveSupport::Concern
def your_fat_method
# insert...
end
end
And in your model simply:
include YourFunctionality

Difficulty aliasing `is_x?` to `has_role? x`

Each user has many roles; to find out whether a user has the "admin" role, we can use the has_role? method:
some_user.has_role?('admin')
Which is defined like this:
def has_role?(role_in_question)
roles.map(&:name).include?(role_in_question.to_s)
end
I'd like to be able to write some_user.has_role?('admin') as some_user.is_admin?, so I did:
def method_missing(method, *args)
if method.to_s.match(/^is_(\w+)[?]$/)
has_role? $1
else
super
end
end
This works fine for the some_user.is_admin? case, but fails when I try to call it on a user referenced in another association:
>> Annotation.first.created_by.is_admin?
NoMethodError: undefined method `is_admin?' for "KKadue":User
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/associations/association_proxy.rb:215:in `method_missing'
from (irb):345
from :0
What gives?
Rails checks if you respond_to? "is_admin?" before doing a send.
So you need to specialize respond_to? also like:
def respond_to?(method, include_private=false)
super || method.to_s.match(/^is_(\w+)[?]$/)
end
Note: Don't ask me why rails checks for respond_to? instead of just doing a send there, I don't see a good reason.
Also: The best way (Ruby 1.9.2+) is to define respond_to_missing? instead, and you can be compatible with all versions with something a bit fancy like:
def respond_to_missing?(method, include_private=false)
method.to_s.match(/^is_(\w+)[?]$/)
end
unless 42.respond_to?(:respond_to_missing?) # needed for Ruby before 1.9.2:
def respond_to?(method, include_private=false)
super || respond_to_missing?(method, include_private)
end
end
The ActiveRecord::Associations::AssociationProxy class overrides method_missing and intercepts the call you are looking for before it gets to the model.
This happens because AP checks if the model respond_to? the method, which in your case, it doesn't.
You have a few solutions aside from editing Rails' source:
First, manually define each of the is_* methods for the user object using metaprogramming. Something like:
class User
Role.all.each do |role|
define_method "is_#{role.name}?" do
has_role?(role.name)
end
end
end
Another is to load the User object via some other means such as
User.find(Annotation.first.user_id).is_admin?
Or use one of the other answers listed.

Resources