I'm attempting to create a custom validation for one of my models in Rails 2.3.5, but I keep recieving the following error everytime I run my testing suite:
`method_missing_without_paginate': undefined local variable or method `validates_progression'
app/models/project.rb
class Project < ActiveRecord::Base
...
validates_progression
def validates_progression
true # stubtastic!
end
end
I can't seem to make much of this~
It doesn't work because you are defining a method with instance scope and you are trying to call it within the class scope. You have two alternatives:
Instance Scope
class Project < ActiveRecord::Base
validate :validates_progression
def validates_progression
true # stub
end
end
Class scope
class Project < ActiveRecord::Base
def self.validates_progression
true # stub
end
# Be sure to define this method before calling it
validates_progression
end
The second alternative doesn't really makes sense unless you want to wrap an other filter.
class Project < ActiveRecord::Base
def self.validates_progression
validates_presence_of :progression
validates_length_of ...
end
# Be sure to define this method before calling it
validates_progression
end
Otherwise, go with the first solution.
The pagination reference is a red herring. The clue is the 'without'. The will paginate gem has aliased the existing method_missing method and called it method_missing_without_pagination. So, the problem is a standard missing method error.
The method is missing because it is a) not defined when you call it and b) not in the correct scope (you are trying to call an instance method in the scope of the class).
You can add your custom validation by using validate with the symbol for your validation method:
validate :validates_progression
def validates_progression
true
end
Related
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 a custom module which sets up a hash to be stored in my sql. As part of this it rolls a its own _changed accessor.
module MyAwesomeCustomModule
extend ActiveSupport::Concern
included do
after_save: wipe_preferences_changed
end
module ClassMethods
def blah
end
etc
end
end
and then in my model:
class MyModel < ActiveRecord::Base
include MyAwesomeCustomModule
after_save :something_that_expects_preferences_changed_to_be_available
blah
end
unfortunately, the after_save defined in the custom module runs before the one defined in the model. Is there a way to get the array of all callbacks and append to it? Is there a way to write a custom after_after_save callback? Is there a way to specify priority/ordering of after_save callbacks?
What would be a good way to resolve this race condition?
In spite of the order of model callbacks, the current design makes the module and the class very coupled.
To solve the current problem as well as improve design, you can define an expected callback in the module's method, and then the class who includes this module is free to respond it or not.
module MyAwesomeCustomModule
extend ActiveSupport::Concern
included do
after_save: wipe_preferences_changed
end
def wipe_preferences_changed
# previous logic to wipe
process_further if respond_to :process_further
end
end
class MyModel < ActiveRecord::Base
include MyAwesomeCustomModule
# Feel free to write this or not
# The content is the previous
# :something_that_expects_preferences_changed_to_be_available
def process_further
end
end
If you want to keep your original strategy (2 after_save callbacks) all you should need to do is move the include statement below the model after_save.
class MyModel < ActiveRecord::Base
after_save :something_that_expects_preferences_changed_to_be_available
include MyAwesomeCustomModule
blah
end
Callbacks are executed in the order they are defined. The include statement acts (very roughly) like you had copy and pasted the code from the module at that point, so by putting the include statement above the after_save in your model you were causing that callback to execute first.
I am using Ruby on Rails 3.2.13 and I would like to properly use the alias_method_chain :build, :option_name statement since I am getting a strange error. That is, ...
... in my controller file I have:
class Articles::CommentsController < ApplicationController
def create
#articles_comment = #article.comments.build(params[:comment])
...
end
end
... in my model file I have:
class Articles::Comment < ActiveRecord::Base
def self.build_with_option_name
...
end
alias_method_chain :build, :option_name
end
When I run the create controller action I get the following error in log:
ActionController::RoutingError (undefined method `build' for class `Articles::Comment'):
app/models/articles/comment.rb:5:in `<class:Comment>'
How should I use the alias_method_chain for the build method? Or, maybe better, should I proceed in another way to reach what I would like to make (for example, should I overwrite the build method in the Articles::Comment model instead of using alias_method_chain)?
Note I: I don't know if it helps, but the build method refers to an association (#article.comments). More, I do not state the build method in the Articles::Comment model because it should be "added" / "attached" to the class by the Ruby on Rails framework itself (I think it is made through meta-programming).
Note II: The same error occurs when considering the new method instead of build; that is, when using alias_method_chain :new, :option_name.
As you said, build is a method defined on association proxy. What you can do is to use association extensions, so in a model you can pass a block to your has_many call, which will be treated as an extension for given association_proxy:
class Article < ActiveRecord::Base
...
has_many :comments do
alias_method_chain :build, :option_name
end
class Country < ActiveRecord::Base
#alias_method :name, :langEN # here fails
#alias_method :name=, :langEN=
#attr_accessible :name
def name; langEN end # here works
end
In first call alias_method fails with:
NameError: undefined method `langEN' for class `Country'
I mean it fails when I do for example Country.first.
But in console I can call Country.first.langEN successfully, and see that second call also works.
What am I missing?
ActiveRecord uses method_missing (AFAIK via ActiveModel::AttributeMethods#method_missing) to create attribute accessor and mutator methods the first time they're called. That means that there is no langEN method when you call alias_method and alias_method :name, :langEN fails with your "undefined method" error. Doing the aliasing explicitly:
def name
langEN
end
works because the langEN method will be created (by method_missing) the first time you try to call it.
Rails offers alias_attribute:
alias_attribute(new_name, old_name)
Allows you to make aliases for attributes, which includes getter, setter, and query methods.
which you can use instead:
alias_attribute :name, :langEN
The built-in method_missing will know about aliases registered with alias_attribute and will set up the appropriate aliases as needed.
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.