Rails console includes differently than model - ruby-on-rails

tl;dr: include MyModule brings MyModule's functions into scope when run in rails console, but not when in a rails model. I don't understand and would like to be able to access these functions in my model.
I have this file lib/zip_validator.rb, which I would like to use to validate user input for my User model.
module ActiveModel::Validations::HelperMethods
def validates_zip(*attr_names)
validates_with ZipValidator, _merge_attributes( attr_names )
end
end
class ZipValidator < ActiveModel::EachValidator
def validate_each( record, attr_name, value )
unless is_legitimate_zipcode( self.zip )
record.errors.add( attr_name, :zip, options.merge( value: value ))
end
end
end
In rails console, I can do
irb(main):005:0> include ActiveModel::Validations::HelperMethods
=> Object
irb(main):006:0> validates_zip
NoMethodError: undefined method `validates_with' for main:Object
from /home/bistenes/Programming/myapp/lib/zip_validator.rb:3:in `validates_zip'
from (irb):6
from /usr/lib64/ruby/gems/1.9.1/gems/railties-3.2.13/lib/rails/commands/console.rb:47:in `start'
from /usr/lib64/ruby/gems/1.9.1/gems/railties-3.2.13/lib/rails/commands/console.rb:8:in `start'
from /usr/lib64/ruby/gems/1.9.1/gems/railties-3.2.13/lib/rails/commands.rb:41:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'
Look! Clearly it found validates_zip, because it's complaining about a call within that method. Maybe that complaint will go away when I actually try and call it from the model where it's going to be used, app/models/user.rb:
class User < ActiveRecord::Base
include ActiveModel::Validations::HelperMethods
attr_accessible :first_name, :last_name, :zip, :email, :password,
:password_confirmation, :remember_me, :confirmed_at
validates_presence_of :first_name, :last_name, :zip
validates_zip :zip
When I attempt to start the server (Thin), it errors out with:
/usr/lib64/ruby/gems/1.9.1/gems/activerecord-3.2.13/lib/active_record/dynamic_matchers.rb:55:in `method_missing': undefined method `validates_zip' for #<Class:0x00000003e03f28> (NoMethodError)
from /home/bistenes/Programming/myapp/app/models/user.rb:38:in `<class:User>'
What in the world is going on? Why does the server fail to find a function that the console found?

Consider this:
module M
def m
end
end
class C
include M
end
Given that, you can say C.new.m without complaint but you can't C.m. So what's going on here? When you say include M, the methods in M will be added to C as instance methods but if you wanted to say:
class C
include M
m
end
then m would have to be a class method since self when you call m here is the class itself. You can do this using the included hook in the module:
module M
def self.included(base)
base.class_exec do
def m
end
end
end
end
class C
include M
m # This works now
end
In your case, you'd have something like this:
module ActiveModel::Validations::HelperMethods
def self.included(base)
base.class_exec do
def self.validates_zip(*attr_names)
validates_with ZipValidator, _merge_attributes( attr_names )
end
end
end
end
Now your User will have a validates_zip class method that you can call in the desired context.

Related

Rails delegate and alias - infinite loop?

I don't understand the following infinite loop involving delegate and alias
class Company
field :name
end
class Employee < Professional
include CompanyMember
end
class Professional
include UserProfile
end
module CompanyMember
belongs_to :company
delegate :name, to: :company, prefix: true
alias :organization_name :company_name
end
module UserProfile
def to_s
out = "#{name} "
out += "(#{organization_name})" if respond_to?(:organization_name)
end
def inspect
to_s + super
end
end
I have an Employee with a missing company, and I have the following infinite loop
app/models/concerns/user_profile.rb:94:in `inspect'
app/models/concerns/company_member.rb:8:in `rescue in company_name'
app/models/concerns/company_member.rb:8:in `company_name'
app/models/concerns/user_profile.rb:89:in `to_s'
app/models/concerns/user_profile.rb:94:in `inspect'
app/models/concerns/company_member.rb:8:in `rescue in company_name'
app/models/concerns/company_member.rb:8:in `company_name'
app/models/concerns/user_profile.rb:89:in `to_s'
The problem is in your override of inspect. When you attempt to call a delegated name on a missing company, NoMethodError is raised. Delegated method then tries to rescue it and show you helpful error message.
exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
You see, it calls inspect to get printable version of your object. Unfortunately, it calls .to_s, which is where infinite recursion begins.

Creating custom getters and setters dynamically under STI in Rails

I am relatively new to meta programming and I am trying to define getters and setters in children models under a single table inheritance, like so:
# app/models/player.rb
class Player < ActiveRecord::Base
serialize :equipments
protected
# Define getters for setters for individual sports
def attr_equipments(*attributes)
attributes.each do |attribute|
define_singleton_method(attribute) do
self.equipments[attribute.to_sym]
end
define_singleton_method("#{attribute}=") do |value|
self.equipments[attribute.to_sym] = value
end
end
end
end
# app/models/baseball_player.rb
class BaseballPlayer < Player
after_initialize do
attr_equipments :bat, :glove
end
end
On a higher layer I would like to do something like this:
# app/controllers/baseball_players_controller.rb
class BaseballPlayersController < ApplicationController
def new
#baseball_player = BaseballPlayer
end
end
# app/views/baseball_players/new.html.haml
=form_for(#baseball_player) do |f|
=f.text_field :bat
=f.text_field :glove
=f.submit
But with the example above I cannot get or set the attributes even from the console.
> p = BaseballPlayer.new
> p.bat
> # returns NoMethodError: undefined method `[]' for nil:NilClass
> p = BaseballPlayer.new
> p.bat = "Awesome bat"
> # returns ArgumentError: wrong number of arguments (0 for 2..3)
Could someone help me find the right direction? Many thanks in advance.
I just solved the issue myself..
For future references, this part needed to be store as serialize does not automatically define any accessors.
# app/models/player.rb
class Player < ActiveRecord::Base
# serialize :equipments -> does not create a setter or getter needed
store :equipments
I also encountered an error in create method in the controller:
def create
#alarm = BaseballPlayer.new(alarm_params)
# Error: undefined attribute 'bat'
which was solved by:
def create
#alarm = BaseballPlayer.new
#alarm.assign_attributes(alarm_params)

Undefined class method using Rails Concerns

I did everything pretty much as described here: question
But I keep getting error:
NoMethodError: undefined method `parent_model' for Stream (call 'Stream.connection' to establish a connection):Class
In model/concerns faculty_block.rb
module FacultyBlock
extend ActiveSupport::Concern
included do
def find_faculty
resource = self
until resource.respond_to?(:faculty)
resource = resource.parent
end
resource.faculty
end
def parent
self.send(self.class.parent)
end
end
module ClassMethods
def parent_model(model)
##parent = model
end
end
end
[Program, Stream, Course, Department, Teacher].each do |model|
model.send(:include, FacultyBlock)
model.send(:extend, FacultyBlock::ClassMethods) # I added this just to try
end
In initializers:
require "faculty_block"
method call:
class Stream < ActiveRecord::Base
parent_model :program
end
It seems that the Stream is loaded before loading concern, make sure that you have applied the concerns inside the class definition. When rails loader matches class name for Stream constant, it autoloads it before the finishing evaliation of the faculty_block, so replace constants in it with symbols:
[:Program, :Stream, :Course, :Department, :Teacher].each do |sym|
model = sym.to_s.constantize
model.send(:include, FacultyBlock)
model.send(:extend, FacultyBlock::ClassMethods) # I added this just to try
end

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

List of attributes from class and include module

I have a class (ActorClue) that has three attr_accessor defined within it. There are a couple other common fields needed by other classes, so I put those common fields in a module (BaseClueProperties). I am including that module within my ActorClue class.
Here's the code sampling :
module BaseClueProperties
attr_accessor :image_url
attr_accessor :summary_text
end
################
class BaseClue
# http://stackoverflow.com/questions/2487333/fastest-one-liner-way-to-list-attr-accessors in-ruby
def self.attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def self.attributes
#attributes
end
def attributes
self.class.attributes
end
end
###############
class ActorClue < BaseClue
attr_accessor :actor_name
attr_accessor :movie_name
attr_accessor :directed_by
include BaseClueProperties
.....
end
I instantiate the above with the following :
>> gg = ActorClue.new
=> #<ActorClue:0x23bf784>
>> gg.attributes
=> [:actor_name, :movie_name, :directed_by]
Why is it only returning :actor_name, :movie_name, and :directed_by and not include :image_url and :summary_text?
I modified the BaseClueProperties to read the following :
module BaseClueProperties
BaseClue.attr_accessor :image_url
BaseClue.attr_accessor :summary_text
end
But still with the same result.
Any thoughts as to why my :image_url and :summary_text attributes aren't getting added to my #attributes collection?
I can't promise that my description of the reason is correct but the following code should fix the problem. I believe that your are adding attributes to the Module not to the class that it is included within. Anyhow replacing your module with the following should fix the problem.
module BaseClueProperties
def self.included(base)
base.send :attr_accessor, :image_url
base.send :attr_accessor, :summary_text
end
end
This should cause the including object to define attribute_accessors when it includes the module.

Resources