Rails metaprogramming 10 x 0 Me - ruby-on-rails

I'm trying do dynamically validate an object.
On my app, a user can create questions that will be part of a form, and each question can have
validations.
So, I post this form, and pass the param to the following class:
require 'ostruct'
class QuestionResponse < OpenStruct
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
extend ActiveModel::Callbacks
def fields
#table.keys
end
def add_validators
stored_questions = AdmissionForm.find(self.form_id).questions.all
questions = fields.select{|f| f.to_s[0]=="q"}
questions.each do |question_param|
question = stored_questions.select{|f| f["id"] == question_param.to_s.gsub("q_","").to_i}.first
unless question.validations.empty?
validations = "validates :#{question_param} , #{question.validations.join(",")}"
self.class.instance_eval validations
end
end
end
def initialize(*args)
super
add_validators if self.fields.any?
end
def persisted? ; false ; end;
end
It almost works.
My problem is that subsequent form posts, concatenate ActiveModel::Errors
#<ActiveModel::Errors:0x00000004432520
#base=#<QuestionResponse q_7="", q_6="", form_id="1">,
#messages=
{:q_7=>["cant be blank", "cant be blank"],
:q_6=>["cant be blank", "cant be blank"]}>
What am I doing wrong?
Thanks!
Alex

add_validators gets called on every instance of QuestionResponse, which adds validations to the class of QuestionResponse. Each new instance adds it's own validations to the class, but you still have the ones added by other (previously created) instances.

Related

How to implement `dry-validation` gem in a Rails form object?

I'm trying to substitute ActiveRecord validations with Dry-validations, but I've been unable to find any in-app implementation examples to follow.
Dry-validation docs: http://dry-rb.org/gems/dry-validation/
I've added below to the form object, but I don't understand how to actually implement it so the validation fails if title is not inputted in the UI's form.
schema = Dry::Validation.Schema do
required(:title).filled
end
Form Object (setup with Virtus):
class PositionForm
include Virtus.model
include ActiveModel::Model
require 'dry-validation'
require 'dry/validation/schema/form'
# ATTRIBUTES
attribute :id, Integer
attribute :title, String
...
# OLD ACTIVE RECORD VALIDATIONS
#validates :title, presence: true
# NEW DRY VALIDATIONS
schema = Dry::Validation.Schema do
required(:title).filled
end
def save
if valid?
persist!
true
else
false
end
end
def persist!
#position = Position.create!(title: title...)
end
end
I've never used dry-validation before - any guidance would be super appreciated!
UPDATE
I've been able to "make it work" but it still doesn't feel like the correct design pattern.
Updated save method
def save
schema = Dry::Validation.Schema do
required(:title).filled
end
errors = schema.(title: title)
if valid? && errors.messages.empty?
persist!
true
else
false
end
end
If anyone could share guidance on the appropriate design pattern to implement dry-validation into a virtus-style form object it would be super appreciated!
I would try to keep validation at the model level.
Have a ModelValidations model in your initializers, each method named after the model it validates.
config/initialize/model_validations.rb
module ModelValidations
def position_form
Dry::Validation.Schema do
required(:title).filled
end
end
end
In the model, call the dry_validation module for that model.
app/models/position_form.rb
class PositionForm
validates :dry_validation
def dry_validation
ModelValidations.position_form(attributes).each do |field, message|
errors.add(field, message)
end
end
end

Ruby on Rails plugin - Adding validation in class doesn't work properly

I made a small DRY plugin that I want to use in some of my classes (like theatre, coffeeshop, restaurant, etc) later on. All those classes consist of an address and therefore, I made an Address model plugin with an act_as_address method that are to be called by those classes to avoid writing duplicate code.
The classes will consist of some validations on the address fields (like presence and length) and I used class_eval inside the act_as_address in the Address model to write those validations to the classes. However, it does not seem to work.
Here is my code and tests:
# structure.sql
create table addresses (
some_info varchar(25)
# other attrs
);
create table theatres (
id integer,
primary key (id)
) inherits (addresses);
# act_as_address.rb
module Address
module ActsAsAddress
extend ActiveSupport::Concern
include do
end
module ClassMethods
def acts_as_address
class_eval do <<-EVAL
validates :some_info, presence: true
# some other validations, methods etc.
EVAL
end
end
end
end
end
ActiveRecord::Base.send :include, Address::ActsAsAddress
# theatre.rb
class Theatre < ActiveRecord::Base
acts_as_address
end
# address.rb
require 'address/acts_as_address'
module Address
end
# acts_as_address_test.rb
require File.expand_path('../test_helper', __FILE__)
class ActsAsAddressTest < ActiveSupport::TestCase
test "should not be valid" do
assert_not Theatre.create(:some_info => nil).valid?,
"some_info was valid with nil value"
end
end
And the result of the test is as following:
1) Failure:
ActsAsAddressTest#test_should_not_be_valid [acts_as_address_test.rb:16]:
some_info was valid with nil value
Could anyone help me out here? Is class_eval the problem here? I am using Ruby on Rails 4.
You may be able to do this:
module Address
module ActsAsAddress
extend ActiveSupport::Concern
module ClassMethods
def acts_as_address
validates :some_info, presence: true
end
end
end
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

How to append errors to a class using the 'ActiveModel::Errors' module

I am using Ruby on Rails 3 and I am trying to exend a class Account in order to handle errors "a la Rails way".
In my model I have
class Users::Account
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::Validations
include ActiveModel::Conversion
def persisted?
false
end
attr_reader :errors
def initialize(attributes = {})
#errors = ActiveModel::Errors.new(self)
#firstname = attributes[:firstname]
#lastname = attributes[:lastname]
...
end
end
I would like to "encapsulate" in the above class the following hash using the ActiveModel::Errors
---
errors:
base: Invalid account.
firstname: Too short.
so that I can do, after inserting the above error hash in the class, like this
#account.errors # => Hash of errors
A debug for a testing scenario is (always) the following because I don't know how to append errors to the class.
firstname: T
lastname: Test surname
errors: !omap []
How can I do that?
you can actually just do
somemodel_instance.errors.add(:some_attr, "some error message")
for more info, refer to here, hope this helps =)

Is there a way to validate a specific attribute on an ActiveRecord without instantiating an object first?

For example, if I have a user model and I need to validate login only (which can happen when validating a form via ajax), it would be great if I use the same model validations defined in the User model without actually instantiating a User instance.
So in the controller I'd be able to write code like
User.valid_attribute?(:login, "login value")
Is there anyway I can do this?
Since validations operate on instances (and they use the errors attribute of an instance as a container for error messages), you can't use them without having the object instantiated. Having said that, you can hide this needed behaviour into a class method:
class User < ActiveRecord::Base
def self.valid_attribute?(attr, value)
mock = self.new(attr => value)
unless mock.valid?
return mock.errors.has_key?(attr)
end
true
end
end
Now, you can call
User.valid_attribute?(:login, "login value")
just as you intended.
(Ideally, you'd include that class method directly into the ActiveRecord::Base so it would be available to every model.)
Thank you Milan for your suggestion. Inspired by it I created a simple module one can use to add this functionality to any class. Note that the original Milans suggestion has a logic error as line:
return mock.errors.has_key?(attr)
should clearly be:
return (not mock.errors.has_key?(attr))
I've tested my solution and it should work, but ofc I give no guarantees. And here's my glorious solution. Basically a 2-liner if you take away the module stuff.. It accepts method names as stings or symbols.
module SingleAttributeValidation
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def valid_attribute?(attr, value)
mock = self.new(attr => value)
(not mock.valid?) && (not mock.errors.has_key?(attr.class == Symbol ? attr : attr.to_sym))
end
end
end
To use your standard validation routines:
User.new(:login => 'login_value').valid?
If that does not work for you, build a custom class method for this:
class User < ActiveRecord::Base
validate do |user|
user.errors.add('existing') unless User.valid_login?(user.login)
end
def self.valid_login?(login)
# your validation here
!User.exist?(:login=> login)
end
end
I had a hell of a time getting this to work in Rails 3.1. This finally worked. (Not sure if it's the best way to do it, I'm kind of a newb.). The problem I was having was that value was being set to type ActiveSupport::SafeBuffer, and was failing validation.
def self.valid_attribute?(attr, value)
mock = User.new(attr => "#{value}") # Rails3 SafeBuffer messes up validation
unless mock.valid?
return (not mock.errors.messages.has_key?(attr))
end
return true
end
I have gone with the custom class solution but I just wanted to make sure there was no better way
class ModelValidator
def self.validate_atrribute(klass, attribute, value)
obj = Klass.new
obj.send("#{attribute}=", value)
obj.valid?
errors = obj.errors.on(attribute).to_a
return (errors.length > 0), errors
end
end
and I can use it like
valid, errors = ModelValidator.validate_attribute(User, "login", "humanzz")
class User < ActiveRecord::Base
validates_each :login do |record, attr, value|
record.errors.add attr, 'error message here' unless User.valid_login?(value)
end
def self.valid_login?(login)
# do validation
end
end
Just call User.valid_login?(login) to see if login itself is valid
An implementation of the 'valid_attribute' method you are suggesting:
class ActiveRecord:Base
def self.valid_attribute?(attribute, value)
instance = new
instance[attribute] = value
instance.valid?
list_of_errors = instance.errors.instance_variable_get('#errors')[attribute]
list_of_errors && list_of_errors.size == 0
end
end
How about:
User.columns_hash.has_key?('login')

Resources