Ruby on Rails - Share one method with 2 models - ruby-on-rails

I have the following module
module SharedMethods
# Class method
module ClassMethods
#
# Remove white space from end of strings
def remove_whitespace
self.attributes.each do |key,value|
if value.kind_of?(String) && !value.blank?
write_attribute key, value.strip
end
end
end
end
#
#
def self.included(base)
base.extend(ClassMethods)
end
end
and I am using it in my models like
include SharedMethods
before_validation :remove_whitespace
However whenever I submit the form I get a "undefined method `remove_whitespace'" message
What can I do to correct this error?

That's because :remove_whitespace must be an instance method, not a class method.
module SharedMethods
def self.included(base)
base.send :include, InstanceMethods
end
module InstanceMethods
# Remove white space from end of strings
def remove_whitespace
self.attributes.each do |key,value|
if value.kind_of(String) && !value.blank?
write_attribute key, value.strip
end
end
end
end
end
Unless you need the module to provide both class and instance methods, you can also skip the usage of self.included and simplify your module in this way:
module SharedMethods
# Remove white space from end of strings
def remove_whitespace
self.attributes.each do |key,value|
if value.kind_of(String) && !value.blank?
write_attribute key, value.strip
end
end
end
end

Related

Ruby Module's included hook instead of regular extending?

There is a general way of adding class methods from Module via its included hook, and following extending base class with ClassMethods submodule. This way is described in book "Metaprogramming Ruby 2: Program Like the Ruby Pros". Here is an example from there:
module CheckedAttributes
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def attr_checked(attribute, &validation)
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' unless validation.call(value)
instance_variable_set("##{attribute}", value)
end
define_method attribute do
instance_variable_get "##{attribute}"
end
end
end
end
class Person
include CheckedAttributes
attr_checked :age do |v|
v >= 18
end
end
But what is the reason of including the almost empty module first, and then extending its includer with one more module? Why not extend the class right the way with target module itself?
module CheckedAttributes
def attr_checked(attribute, &validation)
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' unless validation.call(value)
instance_variable_set("##{attribute}", value)
end
define_method attribute do
instance_variable_get "##{attribute}"
end
end
end
class Person
extend CheckedAttributes
attr_checked :age do |v|
v >= 18
end
end
Is code above totally equal to initial example from this book? Or there are any pitfalls?
I have no idea where you took this code from, but this pattern involving ClassMethods is normally used in the cases when you want to alter both class and eigenclass to avoid the necessity to call both include Foo and extend Bar.
module Named
def self.included(base)
base.extend ClassMethods
end
def describe
"Person is: #{name}"
end
module ClassMethods
def name!
define_method "name=" do |value|
raise 'Invalid attribute' unless validation.call(value)
instance_variable_set("#name", value)
end
define_method "name" do
instance_variable_get "#name"
end
end
end
end
class Person
include Named
name!
end
p = Person.new
p.name = "Trump"
p.describe #⇒ "Person is: Trump"
In your example, it makes zero sense.

How and where to define a method for use in the controller?

I wish to use a method in the controller:
class Hash
def sort_by_array a; Hash[sort_by{|k, _| a.index(k) || length}] end
end
But after placing the code in the controller, I receive an error: class definition in method body
I tried removing the class Hash, and second end, and have also tried
class Hash
def self.class.sort_by_array a; Hash[sort_by{|k, _| a.index(k) || length}] end
end
But I still can't get it to stop erroring
For reference, here is the controller:
class StaticPagesController < ApplicationController
def main
class Hash
def self.class.sort_by_array a; Hash[sort_by{|k, _| a.index(k) || length}] end
end
#languages = Listing.group_by(&:language)
#languages.sort_by_array(#languages)
end
end
That error occurs when you define a class inside the method of another class. i.e. you were probably doing something like below:
class SomeClass
def some_method
class Hash
def sort_by_array(a)
end
end
end
end
Assuming you want to extend the functionality of Hash objects by adding a method sort_by_array, then you can do monkey-patching like below:
Solution (Simple):
you can only define "instance" methods. If you want to define also "class" methods, see "Advanced" solution below.
lib/extensions/hash.rb
module Extensions
module Hash
def sort_by_array(a)
sort_by do |k, _|
a.index(k) || length
end
end
end
end
i.e. let's say another class you want to extend functionality:
lib/extensions/active_record/base.rb
module Extensions
module ActiveRecord
module Base
def say_hello_world
puts 'Hello World!'
end
end
end
end
config/initializers/extensions.rb
Hash.include Extensions::Hash
ActiveRecord::Base.include Extensions::ActiveRecord::Base
Usage:
# rails console
some_array = [:a, :c, :b]
some_hash = { a: 1, b: 2, c: 3 }
some_hash.sort_by_array(some_array)
# => [[:a, 1], [:c, 3], [:b, 2]]
user = User.find(1)
user.say_hello_world
# => 'Hello World!'
Solution (Advanced):
now allows both "class" and "instance" methods to be defined:
lib/extensions/hash.rb
module Extensions
module Hash
def self.included(base)
base.extend ClassMethods
base.include InstanceMethods
end
# define your Hash "class methods" here inside ClassMethods
module ClassMethods
# commented out because not yet fully working (check update later)
# # feel free to remove this part (see P.S. for details)
# def self.extended(base)
# instance_methods.each do |method_name|
# raise NameError, "#{method_name} method already defined!" if (base.singleton_methods - instance_methods).include? method_name
# end
# end
end
# define your Hash "instance methods" here inside InstanceMethods
module InstanceMethods
# commented out because not yet fully working (check update later)
# # feel free to remove this part (see P.S. for details)
# def self.included(base)
# instance_methods.each do |method_name|
# raise NameError, "#{method_name} method already defined!" if (base.instance_methods - instance_methods).include? method_name
# end
# end
def sort_by_array(a)
sort_by do |k, _|
a.index(k) || length
end
end
end
end
end
i.e. let's say another class you want to extend functionality:
lib/extensions/active_record/base.rb
module Extensions
module ActiveRecord
module Base
def self.included(base)
base.extend ClassMethods
base.include InstanceMethods
end
module ClassMethods
# commented out because not yet fully working (check update later)
# # feel free to remove this part (see P.S. for details)
# def self.extended(base)
# instance_methods.each do |method_name|
# raise NameError, "#{method_name} method already defined!" if (base.singleton_methods - instance_methods).include? method_name
# end
# end
def say_hello_mars
puts 'Hello Mars!'
end
end
module InstanceMethods
# commented out because not yet fully working (check update later)
# # feel free to remove this part (see P.S. for details)
# def self.included(base)
# instance_methods.each do |method_name|
# raise NameError, "#{method_name} method already defined!" if (base.instance_methods - instance_methods).include? method_name
# end
# end
def say_hello_world
puts 'Hello World!'
end
end
end
end
end
config/initializers/extensions.rb
Hash.include Extensions::Hash
ActiveRecord::Base.include Extensions::ActiveRecord::Base
Usage:
# rails console
some_array = [:a, :c, :b]
some_hash = { a: 1, b: 2, c: 3 }
some_hash.sort_by_array(some_array)
# => [[:a, 1], [:c, 3], [:b, 2]]
user = User.find(1)
user.say_hello_world
# => 'Hello World!'
ActiveRecord::Base.say_hello_mars
# => 'Hello Mars!'
P.S, arguably you won't need to raise an error if a method is already defined, but this is just my personal taste to prevent "bugs" (i.e. if for example some "gems" you used also defined same exact methods name but having different functionality, of which you have no control of). Feel free to remove them though.
Place it in a separate file. This would extend the base class Hash and would allow you to use in the whole application.
The easiest would be putting the code into config/initializers/hash.rb and restarting the server.
Or, better, similarly to how Rails does it (e.g. https://github.com/rails/rails/tree/master/activesupport/lib/active_support/core_ext):
Put your code here: lib/core_ext/hash/sort_by_array.rb
And then either add this path to autoload, or require it manually from where you'd like to use it, like this:
require "core_ext/hash/sort_by_array".

Ruby on rails DRY strip whitespace from selective input forms

I'm fairly new to rails so bear with me.
I want to strip whitespace from a selective group of input forms.
But I would like a DRY solution.
So I was thinking there might be a solution such as a helper method, or a custom callback. Or a combination such as before_validation strip_whitespace(:attribute, :attribute2, etc)
Any help is awesome! Thanks!
EDIT
I have this in my model file ...
include ApplicationHelper
strip_whitespace_from_attributes :employer_name
... and I have this in my ApplicationHelper ...
def strip_whitespace_from_attributes(*args)
args.each do |attribute|
attribute.gsub('\s*', '')
end
end
but now I'm getting the error message:
undefined method `strip_whitespace_from_attributes' for "the test":String
EDIT II -- SUCCESS
I added this StripWhitespace module file to the lib directory
module StripWhitespace
extend ActiveSupport::Concern
module ClassMethods
def strip_whitespace_from_attributes(*args)
args.each do |attribute|
define_method "#{attribute}=" do |value|
#debugger
value = value.gsub(/\s*/, "")
#debugger
super(value)
end
end
end
end
end
ActiveRecord::Base.send(:include, StripWhitespace)
and then added this to any model class this wants to strip whitespace ...
include StripWhitespace
strip_whitespace_from_attributes #add any attributes that need whitespace stripped
I would go with sth like this (not tested):
module Stripper # yeah!
extend ActiveSupport::Concern
module ClassMethods
def strip_attributes(*args)
mod = Module.new
args.each do |attribute|
define_method "#{attribute}=" do |value|
value = value.strip if value.respond_to? :strip
super(value)
end
end
end
include mod
end
end
end
class MyModel < ActiveRecord::Base
include Stripper
strip_attributes :foo, :bar
end
m = MyModel.new
m.foo = ' stripped '
m.foo #=> 'stripped'
If you can get your attributes in to a single array (perhaps there's a [:params] key you can use instead), you can do the following:
class FooController < ApplicationController
before_create strip_whitespace(params)
private
def strip_whitespace(*params)
params.map{ |attr| attr.strip }
end
end

Ruby mixin best practice

New to Ruby\Rails, shame on me :(
I'm developing an engine for personal use (simple admin panel). What I want, is to be able to config my main app's models, like this:
class User < ActiveRecord::Base
include Entropy::Configurable
entropy_config do
form_caption 'Editing user'
end
end
And then in engine's templates do this:
<h1><%= #object.entropy_config :form_caption %></h1>
Engine's module:
module Entropy
module Configurable
def self.included(base)
## to call entropy_config in model class
base.send :extend, ClassMethods
end
def entropy_config(arg)
## ... I'm missing this part
end
module ClassMethods
##config = { ... }
def entropy_config (&block)
class_eval &block
end
def form_caption(arg)
// skipping class identification
##config[:user][:form_caption] = arg
end
end
end
end
The problem is that I can not get access to ##config from Configurable module, actually when I call entropy_config on #object. What I'm doing wrong?
First of all you've doing it wrong. Rails is on of the frameworks that pushed a lot on the MVC architecture. Having your model know about form captions is wrong. For that I would use the rails i18n gem. For the sake of the argument here's some untested code that will probably answer your question:
module Entropy
module Configurable
def self.included(base)
## to call entropy_config in model class
base.send :extend, ClassMethods
end
def entropy_config(key)
self.class.config[:user][key]
end
module ClassMethods
cattr_accessor :config
def entropy_config (&block)
self.config ||= {}
class_eval &block
end
def form_caption(arg)
// skipping class identification
self.config[:user][:form_caption] = arg
end
end
end
end
see http://apidock.com/rails/Class/cattr_accessor for more info

Is there a way to force a "before_destroy" callback to be trigered first?

I have a model which specifies a before destroy callback. Something like:
Class User < ActiveRecord::Base
before_destroy :do_destroy
acts_as_destroyable
private
def do_destroy
puts "A"
end
end
module ActsAsDestroyable
def self.included(base)
base.send(:extend, ActsMethods)
end
module ActsMethods
def acts_as_destroyable(options = {})
self.send(:include, InstanceMethods)
end
module InstanceMethods
def do_something
puts "A0"
end
def self.included(base)
base.before_destroy :do_something
end
end
end
end
Now since the Destroyable module works with Users's assosiacions it needs to have its "do_domething" method executed before the "do_destroy" callback. Any ideas how can I move it up the callback queue?

Resources