Using instance variables inside ActiveModel::EachValidator - ruby-on-rails

I faced with curious fact (for me) about using each validators. For example we have a some custom each validator and some model:
class Thing < ApplicationRecord
validates :field, custom: true
end
class CustomValidator < ActiveModel::EachValidator
def validate_each(record, _attribute, _value)
#record = record
end
end
I've found out that an instance of CustomValidator class will be created once when we call Thing model for the first time. It means that we will have same validator object for every Thing instance. And my question is: How do you think, can we use instance variables inside validators like this or not... because looks like validator object will be created only once, and, for example, if we will call Thing.first.valid? and then Thing.last.valid? the #record will have the same value before we reassign it.
Or maybe a separate validator object will be created for each client?
I just worry is it possible that we can face races around #record variable when several widgets will be validated at the same time?
Thnx

I found another way, just define the class with a class method "validate" and an instance method "validate", and use this class method like a block. So, we can move huge validation in class, use it shortly in model Thing, and use instance variable between validator methods
class Thing < ApplicationRecord
validate MyCustomValidator
end
class MyCustomValidator
def self.validate(record)
new(record).validate
end
def initialize(record)
#record = record
end
def validate
# some payload with #record
validate_breakdowns
validate_indicators
end
private
attr_reader :record
def validate_breakdowns
# some payload with #record
end
def validate_indicators
# some payload with #record
end
end

Related

Calling a helper from a class and instance method in a Rails model

I need to call a helper method within a model, from both a class and an instance method, e.g. Model.method(data) and model_instance.method. However, the class method always returns "NoMethodError: undefined method 'helper_method' for #<Class ...>"
model.rb:
class Model < ActiveRecord::Base
include ModelHelper
def method
helper_method(self.data)
end
def self.method(data)
self.helper_method(data)
end
end
model_helper.rb:
module ModelHelper
def helper_method(data)
# logic here
end
end
I even tried adding def self.helper_method(data) in the helper to no avail.
After quite a bit of seraching, I wasn't able to find anything on how to achieve this, or at least anything that worked.
The answer turned out to be pretty simple, and doesn't require any Rails magic: you just re-include the helper and define the class method within a class block:
class Model < ActiveRecord::Base
include ModelHelper
def method
helper_method(self.data)
end
# Expose Model.method()
class << self
include ModelHelper
def method(data)
helper_method(data)
end
end
end
No changes to the helper needed at all.
Now you can call method on both the class and an instance!
If there's no additional logic in method, then you can simply do:
class Model < ActiveRecord::Base
include ModelHelper
extend ModelHelper
end
And get both the instance (#model.helper_method) and the class (Model.helper_method) methods.
If, for legacy (or other) reasons, you still want to use method as an instance and class method, but method doesn't do anything different than helper_method, then you could do:
class Model < ActiveRecord::Base
include ModelHelper
extend ModelHelper
alias method helper_method
singleton_class.send(:alias_method, :method, :helper_method)
end
And now you can do #model.method and Model.method.
BTW, using modules to include methods in classes is seductive, but can get away from you quickly if you're not careful, leaving you doing a lot of #model.method(:foo).source_location, trying to figure out where something came from. Ask me how I know...
you need to define model_helper.rb as:
module ModelHelper
def self.helper_method(data)
# logic here
end
end
and call this method in model.rb as:
class Model < ActiveRecord::Base
include ModelHelper
def method
ModelHelper.helper_method(self.data)
end
def self.method(data)
ModelHelper.helper_method(data)
end
end

Where is the place for altering validation?

Sometimes I need not to simply validate smth in my app, but also alter it before/after validating.
I.e.
class Channel < ActiveRecord::Base
validate :validate_url
...
private
def validate_url
url = "rtmp://#{url}" if server_url[0..6] != "rtmp://" #alter cause need this prefix
unless /rtmp:\/\/[a-z0-9]{1,3}\.pscp\.tv:80\/[a-z0-9]\/[a-z0-9]{1,3}\//.match url
errors.add(:url, "...")
end
end
end
or smth like this
class Channel < ActiveRecord::Base
validate :validate_restreams
...
private
def validate_restreams
self.left_restreams = self.left_restreams - self.restreams #to be sure there's no intersections
end
end
But I feel it's not a right place for such things, so I need to know what's the way to do it right?
You can create a custom validator for a rails model. You should make a class, inherit it from ActiveModel::Validator, and define a validate(record) method there, which will add errors to the record. For example:
This is your validator class:
class MyValidator < ActiveModel::Validator
def validate(record)
unless url_valid?(record[:url])
record.errors.add(:url, 'is invalid')
end
end
private
def url_valid?(url)
# validate url and return bool
end
end
And now simply add this to the model:
validates_with MyValidator

Creating dynamic instance methods in after_save callback rails

I have a after_save callback in a model named Field and i am creating dynamic instance methods in it on other model named User, but the code is not working, i am unable to figure out whats wrong with it, as the logic is very simple.Please help.
class field < ActiveRecord::Base
after_create :create_user_methods
private
def create_user_methods
User.class_eval do
define_method(self.name) do
#some code
end
define_method(self.name + "=") do
#some code
end
end
end
end
and then I am creating Field instance in rails console like this
Field.create(name: "test_method")
And then calling that method on User class instance like this
User.new.test_method
But it raises error
undefined method test_method for ....
I got the fix, I can not use self inside the class_eval block as its value is User model not Field class object, therefore the fix is:
class field < ActiveRecord::Base
after_create :create_user_methods
private
def create_user_methods
name = self.name # here self points to field object
User.class_eval do
define_method(name) do
#some code
end
define_method(name + "=") do
#some code
end
end
end
end

devise: instance the current_user using single table inheritance

I am using rails 3.0.9 and devise for authentication. Now I'm trying to use single table inheritance because I need to use polymorphism, so I have two classes: UserType1 and UserType2, which inherit from User class. I need that Devise instance correctly the current_user depending the type of user.
For example,
class User < ActiveRecord::Base
#devise and other user logic
end
class UserType1 < User
def get_some_attribute
return "Hello, my type is UserType1"
end
end
class UserType2 < User
def get_some_attribute
return "Hello, my type is UserType2"
end
end
In controller
class MyController < ApplicationController
def action
#message = current_user.get_some_attribute #depending the type using polymorphism
render :my_view
end
end
it's exactly what you need : http://blog.jeffsaracco.com/ruby-on-rails-polymorphic-user-model-with-devise-authentication
you need to override the sign in path method in your application controller, hope it help.
You will need to add get_some_attribute method inside User model
Module User < ActiveRecord::Base
#devise and other user logic
def get_some_attribute
#You can put shared logic between the two users type here
end
end
then, to override it in the user sub types, like this:
Module UserType1 < User
def get_some_attribute
super
return "Hello, my type is UserType1"
end
end
Module UserType2 < User
def get_some_attribute
super
return "Hello, my type is UserType2"
end
end
Then, current_user.get_some_attribute will work as you expecting, if you like to read more about overriding methods in Ruby, you can read about it here
I added super as I assumed that you have some shared logic in get_some_attribute, as it will call get_some_attribute in User model, you can remove it if you don't need it.
Good luck!

Creating Rails ActiveRecord model from params hash

Most Rails tutorials show how to populate a model class via the params hash like so:
class UsersController < ApplicationController
def create
#user = User.create(params[:user])
# more logic for saving user / redirecting / etc.
end
end
This works great if all the attributes in your model are supposed to be strings. However, what happens if some of the attributes are supposed to be ints or dates or some other type?
For instance, let's say the User class looks like this
class User < ActiveRecord::Base
attr_accessible :email, :employment_start_date, :gross_monthly_income
end
The :email attribute should be a string, the :employment_start_date attribute should be a date, and the :gross_monthly_income should be a decimal. In order for these attributes to be of the correct type, do I need to change my controller action to look something like this instead?
class UsersController < ApplicationController
def create
#user = User.new
#user.email = params[:user][:email]
#user.employment_start_date = params[:user][:employment_start_date].convert_to_date
#user.gross_monthly_income = params[:user][:gross_monthly_income].convert_to_decimal
# more logic for saving user / redirecting / etc.
end
end
According to the ActiveRecord documentation, the attributes should automatically be typecasted based on the column types in the database.
I would actually add a before_save callback in your users model to make sure that the values you want are in the correct format i.e.:
class User < ActiveRecord::Base
before_save :convert_values
#...
def convert_values
gross_monthly_income = convert_to_decimal(gross_monthly_income)
#and more conversions
end
end
So you can just call User.new(params[:user]) in your controller, which follows the motto "Keep your controllers skinny"

Resources