Rails Modules: How to define instance methods inside a class method? - ruby-on-rails

I'd like to create a module called StatusesExtension that defines a has_statuses method. When a class extends StatusesExtension, it will have validations, scopes, and accessors for on those statuses. Here's the module:
module StatusesExtension
def has_statuses(*status_names)
validates :status, presence: true, inclusion: { in: status_names }
# Scopes
status_names.each do |status_name|
scope "#{status_name}", where(status: status_name)
end
# Accessors
status_names.each do |status_name|
define_method "#{status_name}?" do
status == status_name
end
end
end
end
Here's an example of a class that extends this module:
def Question < ActiveRecord::Base
extend StatusesExtension
has_statuses :unanswered, :answered, :ignored
end
The problem I'm encountering is that while scopes are being defined, the instance methods (answered?, unanswered?, and ignored?) are not. For example:
> Question.answered
=> [#<Question id: 1, ...>]
> Question.answered.first.answered?
=> false # Should be true
How can I use modules to define both class methods (scopes, validations) and instance methods (accessors) within the context of a single class method (has_statuses) of a module?
Thank you!

As the comments have said, the method is being defined, just not working as expected. I suspect this is because you are comparing a string with a symbol within the method (status_names is an array of symbols, and status will be a string). Try the following:
status_names.each do |status_name|
define_method "#{status_name}?" do
status == status_name.to_s
end
end

Related

How to create a Rails 4 Concern that takes an argument

I have an ActiveRecord class called User. I'm trying to create a concern called Restrictable which takes in some arguments like this:
class User < ActiveRecord::Base
include Restrictable # Would be nice to not need this line
restrictable except: [:id, :name, :email]
end
I want to then provide an instance method called restricted_data which can perform some operation on those arguments and return some data. Example:
user = User.find(1)
user.restricted_data # Returns all columns except :id, :name, :email
How would I go about doing that?
If I understand your question correctly this is about how to write such a concern, and not about the actual return value of restricted_data. I would implement the concern skeleton as such:
require "active_support/concern"
module Restrictable
extend ActiveSupport::Concern
module ClassMethods
attr_reader :restricted
private
def restrictable(except: []) # Alternatively `options = {}`
#restricted = except # Alternatively `options[:except] || []`
end
end
def restricted_data
"This is forbidden: #{self.class.restricted}"
end
end
Then you can:
class C
include Restrictable
restrictable except: [:this, :that, :the_other]
end
c = C.new
c.restricted_data #=> "This is forbidden: [:this, :that, :the_other]"
That would comply with the interface you designed, but the except key is a bit strange because it's actually restricting those values instead of allowing them.
I'd suggest starting with this blog post: https://signalvnoise.com/posts/3372-put-chubby-models-on-a-diet-with-concerns Checkout the second example.
Think of concerns as a module you are mixing in. Not too complicated.
module Restrictable
extend ActiveSupport::Concern
module ClassMethods
def restricted_data(user)
# Do your stuff
end
end
end

Define dynamic class methods

I know this question has been asked several times, but I think never in this conditions.
I try to create an "helper" for my models (an acts_as), for automatically set a status from a status_id.
acts_as_statusable :status, [ :in_progress, closed ]
This helper creates methods status, status=(sym), in_progress?, closed? and named scopes in_progress and closed.
My helper work, this is its code put in lib/
class ActiveRecord::Base
def self.acts_as_statusable(*args)
field_name = args.first
field_name_id = :"#{field_name}_id"
statuses = args.second
# Define getter
define_method(field_name) do
return nil if self.send(field_name_id).nil?
return statuses[self.send(field_name_id)]
end
# Define setter
define_method(:"#{field_name}=") do |v|
return self.send(:"#{field_name_id}=", statuses.index(v))
end
# Define a status? for each status,
# and a named scope .status
statuses.each do |status|
define_method(:"#{status}?") do
return self.send(field_name) == status
end
scope status, -> { where(["#{table_name}.#{field_name_id} = ?", statuses.index(status)]) }
end
validates field_name_id, :inclusion => { :in => (0...statuses.length).to_a }
end
end
Now, the problem
I need to have a Class method, for every class using acts_as_statusable, named status_index(sym) which returns the status_id from a status.
The problem is that every solutions I found for defining a class models are using Module, extend or intend, but I can not insert the statuses variable of the acts_as_statusable line into this modules...
How can I do this ? I use Rails 4.
you can use class_eval, and class_variable_set
class_variable_set :"###{field_name}_options", statuses
class_eval <<-RUBY
def self.status_index(sym)
###{field_name}_options.index(sym)
end
RUBY
which in your case will give a
##status_options class variable
and a
status_index(sym)
method that returns the offset of the given status
xlembouras - your solution works great! I have two models:
class User < ActiveRecord::Base
acts_as_statusable :status, [ :little, :big ]
end
class Price < ActiveRecord::Base
acts_as_statusable :status, [ :in_progress, :closed ]
end
'rails c' execution:
> User.status_index(:little)
=> 0
User.status_index(:big)
=> 1
> Price.status_index(:closed)
=> 1
> Price.status_index(:in_progress)
=> 0
I modified the structure, and used code written by ForgetTheNorm.
Rails 4, ActiveSupport::Concern used
lib/active_record_extension.rb :
module ActiveRecordExtension
extend ActiveSupport::Concern
end
ActiveRecord::Base.send(:include, ActiveRecordExtension)
config/initializers/extensions.rb :
require "active_record_extension"
config/initializers/active_record_monkey_patch.rb
class ActiveRecord::Base
def self.acts_as_statusable(*args)
...
# ForgetTheNorm's CODE..
...
# xlembouras CODE:
class_variable_set :"###{field_name}_options", statuses
class_eval <<-RUBY
def self.status_index(sym)
###{field_name}_options.index(sym)
end
RUBY
end
end

Define class in Rspec

I want to test an inclusion of a module into a class. I am trying define a new class in RSpec:
describe Statusable do
let(:test_class) do
class ModelIncludingStatusable < ActiveRecord::Base
include Statusable
statuses published: "опубликовано", draft: "черновик"
end
end
describe '#statuses' do
it 'sets STATUSES for a model' do
test_class::STATUSES.should == ["опубликовано", "черновик"]
end
end
end
And I get an error:
TypeError:
[ActiveModel::Validations::InclusionValidator] is not a class/module
This is probably because in Statusable I have:
validates_inclusion_of :status, :in => statuses,
:message => "{{value}} должен быть одним из: #{statuses.join ','}"
But if I comment it out, I get:
TypeError:
["опубликовано", "черновик"] is not a class/module
Maybe new class definition isn't the best option, what do I do then? And even if it's not, how can I define a class in RSpec? And how do I fix this error?
Do not define new constant in tests otherwise it will pollute other tests. Instead, use stub_const.
Also, for this is an unit test of Statusable module. If ActiveRecord model is not a necessity, better not to use it.
You can also use class_eval to avoid not opening this class(no matter fake or not) actually
describe Statusable do
before do
stub_const 'Foo', Class.new
Foo.class_eval{ include Statusable }
Foo.class_eval{ statuses published: "foo", draft: "bar"}
end
context '#statuses' do
it 'sets STATUSES for a model' do
FOO::STATUSES.should == ["foo", "bar"]
end
end
end
Though I copied your assertion, I would suggest not to insert a constant say STATUS into the class/module(Foo) who includes this module. Instead, a class method would be better
expect(Foo.status).to eq(["foo", "bar"])
It fails because class definition does not return itself.
$ irb
> class Foo; 1 end
=> 1
you need to do like this:
let(:test_class) do
class ModelIncludingStatusable < ActiveRecord::Base
include Statusable
statuses published: "опубликовано", draft: "черновик"
end
ModelIncludingStatusable # return the class
end
It works but unfortunately, ModelIncludingStatusable will be defined on top-level because of ruby rule.
To capsulize your class, you should do like this:
class self::ModelIncludingStatusable < ActiveRecord::Base
include Statusable
statuses published: "опубликовано", draft: "черновик"
end
let(:test_class) do
self.class::ModelIncludingStatusable # return the class
end
It works perfectly :)
When you call let this define a memoized helper method. You can't class definition in method body.
Another option which I frequently use is to put the entire test in it's own module, e.g.
module Mapping::ModelSpec
module Human
Person = Struct.new(:name, :age, :posessions)
Possession = Struct.new(:name, :value)
end
RSpec.describe Mapping::Model do
it 'can map with base class' do
person = Human::Person.new('Bob Jones', 200, [])
...
end
end
end
While this is a bit cumbersome, it avoids polluting the global namespace, is only slightly more syntax, and is generally easy to understand. Personally, I'd like a better option.. but I'm not sure what that would be.

Calling private method outputs undefined method error

I receive undefined method 'search_type' for the code below. Can you tell me what am I doing wrong here? Probably something with calling private functions, but I can't find what the problem is.
class Entry < ActiveRecord::Base
attr_accessible :content, :rank, :title, :url, :user_id
def self.search(params)
t, o = search_type(params[:type]),search_order(params[:order])
scope = self
scope = scope.where(t) if t
scope.order(o).page(params[:page]).per_page(20)
end
private
def search_order(order)
return 'comments_count DESC' if order == '1'
return 'points DESC' if order == '2'
'rank DESC'
end
def search_type(type)
return nil unless type.present?
"entry_type = #{type}"
end
end
In the controller, I have only #entries = Entry.search(params).
It's not to do with the privateness of your methods, but the fact that search is a class method, so when you call search_order from within it, it is looking for a class method called search_order but you've defined search_order as in instance method.
Make your 2 helper methods class methods and you should be ok. if you want them to be private class methods, then
class << self
def search(...)
end
private
def search_type(...)
end
def search_order(...)
end
end
If you are wondering why #entries.search(...) works it's because I assume that #entries is something like Entry.where(...) ie, a scope and you can call class methods on scopes.
search is defined as a class method, so you should call Entry.search(params) instead of #entries.search(params).
Your method is an a class method, you cant use it form instances of your class

Defining a Rails helper (or non-helper) function for use everywhere, including models

I have a function that does this:
def blank_to_negative(value)
value.is_number? ? value : -1
end
If the value passed is not a number, it converts the value to -1.
I mainly created this function for a certain model, but it doesn't seem appropriate to define this function in any certain model because the scope of applications of this function could obviously extend beyond any one particular model. I'll almost certainly need this function in other models, and probably in views.
What's the most "Rails Way" way to define this function and then use it everywhere, especially in models?
I tried to define it in ApplicationHelper, but it didn't work:
class UserSkill < ActiveRecord::Base
include ApplicationHelper
belongs_to :user
belongs_to :skill
def self.splice_levels(current_proficiency_levels, interest_levels)
Skill.all.reject { |skill| !current_proficiency_levels[skill.id.to_s].is_number? and !interest_levels[skill.id.to_s].is_number? }.collect { |skill| {
:skill_id => skill.id,
:current_proficiency_level => blank_to_negative(current_proficiency_levels[skill.id.to_s]),
:interest_level => blank_to_negative(interest_levels[skill.id.to_s]) }}
end
end
That told me
undefined method `blank_to_negative' for #
I've read that you're "never" supposed to do that kind of thing, anyway, so I'm kind of confused.
if you want to have such a helper method in every class in your project, than you are free to add this as a method to Object or whatever you see fits:
module MyApp
module CoreExtensions
module Object
def blank_to_negative
self.is_number? ? self : -1
end
end
end
end
Object.send :include, MyApp::CoreExtensions::Object
There are a few options:
Monkey-patch the method into ActiveRecord and it will be available across all of your models:
class ActiveRecord::Base
def blank_to_negative(value)
value.is_number? ? value : -1
end
end
Add a "concern" module which you then mix into selected models:
# app/concerns/blank_to_negate.rb
module BlankToNegate
def blank_to_negative(value)
value.is_number? ? value : -1
end
end
# app/models/user_skill.rb
class UserSkill < ActiveRecord::Base
include BlankToNegate
# ...
end
Ruby Datatypes functionality can be extended. They are not sealed. Since you wan to use it in all places why not extend FIXNUM functionality and add a method blank_to_negative to it.
Here's what I ended up doing. I put this code in config/initializers/string_extensions.rb.
class String
def is_number?
true if Float(self) rescue false
end
def negative_if_not_numeric
self.is_number? ? self : -1
end
end
Also, I renamed blank_to_negative to negative_if_not_numeric, since some_string.negative_if_not_numeric makes more sense than some_string.blank_to_negative.

Resources