on an ActiveModel Object, how do I check uniqueness? - ruby-on-rails

In Bryan Helmkamp's excellent blog post called "7 Patterns to Refactor Fat ActiveRecord Models", he mentions using Form Objects to abstract away multi-layer forms and stop using accepts_nested_attributes_for.
Edit: see below for a solution.
I've almost exactly duplicated his code sample, as I had the same problem to solve:
class Signup
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :account
attribute :name, String
attribute :account_name, String
attribute :email, String
validates :email, presence: true
validates :account_name,
uniqueness: { case_sensitive: false },
length: 3..40,
format: { with: /^([a-z0-9\-]+)$/i }
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
end
end
One of the things different in my piece of code, is that I need to validate the uniqueness of the account name (and user e-mail). However, ActiveModel::Validations doesn't have a uniqueness validator, as it's supposed to be a non-database backed variant of ActiveRecord.
I figured there are three ways to handle this:
Write my own method to check this (feels redundant)
Include ActiveRecord::Validations::UniquenessValidator (tried this, didn't get it to work)
Or add the constraint in the data storage layer
I would prefer to use the last one. But then I'm kept wondering how I would implement this.
I could do something like (metaprogramming, I would need to modify some other areas):
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
rescue ActiveRecord::RecordNotUnique
errors.add(:name, "not unique" )
false
end
But now I have two checks running in my class, first I use valid? and then I use a rescue statement for the data storage constraints.
Does anyone know of a good way to handle this issue? Would it be better to perhaps write my own validator for this (but then I'd have two queries to the database, where ideally one would be enough).

Creating a custom validator may be overkill if this just happens to be a one-off requirement.
A simplified approach...
class Signup
(...)
validates :email, presence: true
validates :account_name, length: {within: 3..40}, format: { with: /^([a-z0-9\-]+)$/i }
# Call a private method to verify uniqueness
validate :account_name_is_unique
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
# Refactor as needed
def account_name_is_unique
if Account.where(name: account_name).exists?
errors.add(:account_name, 'Account name is taken')
end
end
def persist!
#account = Account.create!(name: account_name)
#user = #account.users.create!(name: name, email: email)
end
end

Bryan was kind enough to comment on my question to his blog post. With his help, I've come up with the following custom validator:
class UniquenessValidator < ActiveRecord::Validations::UniquenessValidator
def setup(klass)
super
#klass = options[:model] if options[:model]
end
def validate_each(record, attribute, value)
# UniquenessValidator can't be used outside of ActiveRecord instances, here
# we return the exact same error, unless the 'model' option is given.
#
if ! options[:model] && ! record.class.ancestors.include?(ActiveRecord::Base)
raise ArgumentError, "Unknown validator: 'UniquenessValidator'"
# If we're inside an ActiveRecord class, and `model` isn't set, use the
# default behaviour of the validator.
#
elsif ! options[:model]
super
# Custom validator options. The validator can be called in any class, as
# long as it includes `ActiveModel::Validations`. You can tell the validator
# which ActiveRecord based class to check against, using the `model`
# option. Also, if you are using a different attribute name, you can set the
# correct one for the ActiveRecord class using the `attribute` option.
#
else
record_org, attribute_org = record, attribute
attribute = options[:attribute].to_sym if options[:attribute]
record = options[:model].new(attribute => value)
super
if record.errors.any?
record_org.errors.add(attribute_org, :taken,
options.except(:case_sensitive, :scope).merge(value: value))
end
end
end
end
You can use it in your ActiveModel classes like so:
validates :account_name,
uniqueness: { case_sensitive: false, model: Account, attribute: 'name' }
The only problem you'll have with this, is if your custom model class has validations as well. Those validations aren't run when you call Signup.new.save, so you will have to check those some other way. You can always use save(validate: false) inside the above persist! method, but then you have to make sure all validations are in the Signup class, and keep that class up to date, when you change any validations in Account or User.

Related

Rails: Validate input without need of models

Let's say that I have an input field with a value, and I want to validate it (on the server side) to make sure, for instance, that the field has at least 5 characters.
The problem is that it is not something that I want to save in the database, or build a model. I just want to check that the value validates.
In PHP with Laravel, validation is quite easy:
$validator = Validator::make($data, [
'email' => ['required', 'email'],
'message' => ['required']]);
if ($validator->fails()) { // Handle it... }
Is there anything similar in Rails, without need of ActiveRecord, or ActiveModel? Not every data sent from a form makes sense as a Model.
You can use ActiveModel::Validations like this
class MyClass
include ActiveModel::Validations
validates :email, presence: true
validates :message, presence: true
end
It will act as a normal model and you will be able to do my_object.valid? and my_object.errors.
Rails validations live in ActiveModel so doing it without ActiveModel seems kind of counter-productive. Now, if you can loosen that requirement a bit, it is definitely possible.
What I read you asking for, and as I read the PHP code doing, is a validator-object that can be configured on the fly.
We can for example build a validator class dynamically and use instance of that class to run our validations. I have opted for an API that looks similar to the PHP one here:
class DataValidator
def self.make(data, validations)
Class.new do
include ActiveModel::Validations
attr_reader(*validations.keys)
validations.each do |attribute, attribute_validations|
validates attribute, attribute_validations
end
def self.model_name
ActiveModel::Name.new(self, nil, "DataValidator::Validator")
end
def initialize(data)
data.each do |key, value|
self.instance_variable_set("##{key.to_sym}", value)
end
end
end.new(data)
end
end
Using DataValidator.make we can now build instances of classes with the specific validations that we need. For example in a controller:
validator = DataValidator.make(
params,
{
:email => {:presence => true},
:name => {:presence => true}
}
)
if validator.valid?
# Success
else
# Error
end

Creating 2 objects with has_one association in Rails

I need to get some info about creating new objects in Rails with validation. For example, there is the following code:
def create
#user = User.new(params[:user])
if #user.save
# some actions: redirect, render, etc
else
render 'new'
end
end
But if there is 2 models with has_one association, for example Club and Place. I need to create both this objects from params in the same 'create' action, because I've got the same form for inputing data for it(params[:club] and params[:club][:place]). I don't know how I should save this objects, because for building a place (#club.build_place(params[:club][:place])) I should save #club in database. Please, give me example of the code for my problem. Thanks in advance.
If you're creating multiple objects from a single form you'd probably be best off putting this logic into a "Form Object"... See the article "7 Patterns to Refactor Fat ActiveRecord Models" from the CodeClimate blog found here (look for Section #3 on extracting Form Objects): http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models.
Railscasts also has a good episode on form objects, though it is a "Pro Episode" (i.e. requires subscription). http://railscasts.com/episodes/416-form-objects
In short, you create a custom model including some of the necessary ActiveModel modules then create a custom save method, e.g. (this is directly from the article which has a lot of great advice).
class Signup
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :company
attribute :name, String
attribute :company_name, String
attribute :email, String
validates :email, presence: true
# … more validations …
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
#company = Company.create!(name: company_name)
#user = #company.users.create!(name: name, email: email)
end
end
This gives you much more control and a much cleaner interface.

rails 3, set a value on an object if it fails validation

I have an object with multiple validations.
gist of the Approval model: https://gist.github.com/1579150 (side note, I know the Email Domain Validor doesn't work...)
The point is, if these validations fail, I want the object to save, but then set a value on approval.issue = true. Approval.issue is a boolean field that defaults to false, but then if the object fails validations I want the system admin to be able to see it and then handle it appropriately.
To make it more idiot proof, it would be nice to have some validations that can force the user to make changes, but then some would be exempt and would simply trigger the .issue field to true.
For instance, if the email is of the right domain but the email doesn't exist in the system, it would save it but then set issue => true. I could then set up a simple view for Approvals where :issue => :true. then the admin could modify or delete bad Approvals.
Ideas?
Code from gist:
class Approval < ActiveRecord::Base
class ApproverEmailValidator < ActiveModel::EachValidator
def validate_each(approval, attribute, value)
approval.errors[attribute] << "must be a valid e-mail address in our system" unless is_valid_email?(value)
end
protected
def is_valid_email?(address)
User.find_by_email(address)
end
end # End Approver Validator
class EmailDomainValidator < ActiveModel::EachValidator
def email_domain_is?(domain)
unless /ravennainteractive.com$/ =~ email(domain)
errors.add(:email, "You must Use an Eddie Bauer email address")
end
end
end #End Email Domain Validator
belongs_to :recommendation
attr_accessible :approval, :email, :user_id
validates :email, :email_domain
validates :next_approver_email, :approver_email => { :if => :recently_approved? }
before_save :create_next_approval
after_create :approval_notification
attr_accessor :next_approver_email
def recently_approved?
self.approved_changed? && self.approved?
end
def create_next_approval
next_approval = self.recommendation.approvals.build(:email => self.next_approver_email, :user_id => User.find_by_email(next_approver_email))
next_approval.save if next_approver_email.present? && recently_approved?
end
def email_domain_is?
unless /ravennainteractive.com$/ =~ email
errors.add(:email, "You must Use an Eddie Bauer email address")
end
end
private
def approval_notification
ApprovalMailer.needs_approval(self).deliver
end
end
You can implement observer for Approval that will analyze you objects before saving and set issue to "true", if there is some suspicious input.
UPDATE: Here is short guide how to implement observer:
rails generate observer - after this step you`ll see _observer.rb file.
Implement needed methods. Here is simple example extracted from one of my projects (It seems like you should use "before_save" method):
class HomeworkObserver < ActiveRecord::Observer
def after_create(homework)
TeacherMailer.send_later(:student_submitted_homework, homework)
end
def after_save(homework)
if (homework.checked)
StudentMailer.send_later(:teacher_checked_homework, homework)
end
end
end
Also you need to enable observer by adding it to your config/application.rb, e.g:
config.active_record.observers = :homework_observer
Official docs: http://api.rubyonrails.org/classes/ActiveRecord/Observer.html

Validation in Rails without a model

I have a form that allows the user to send a message to an email, and I want to add validation to it. I do not have a model for this, only a controller. How should I do this in Rails?
I was considering doing the validation in the controller, and displaying the errors to the user using the flash object. Is there a better way of doing this?
The best approach would be to wrap up your pseudo-model in a class, and add the validations there. The Rails way states you shouldn't put model behavior on the controllers, the only validations there should be the ones that go with the request itself (authentication, authorization, etc.)
In Rails 2.3+, you can include ActiveRecord::Validations, with the little drawback that you have to define some methods the ActiveRecord layer expects. See this post for a deeper explanation. Code below adapted from that post:
require 'active_record/validations'
class Email
attr_accessor :name, :email
attr_accessor :errors
def initialize(*args)
# Create an Errors object, which is required by validations and to use some view methods.
#errors = ActiveRecord::Errors.new(self)
end
# Required method stubs
def save
end
def save!
end
def new_record?
false
end
def update_attribute
end
# Mix in that validation goodness!
include ActiveRecord::Validations
# Validations! =)
validates_presence_of :name
validates_format_of :email, :with => SOME_EMAIL_REGEXP
end
In Rails3, you have those sexy validations at your disposal :)
For Rails 3+, you should use ActiveModel::Validations to add Rails-style validations to a regular Ruby object.
From the docs:
Active Model Validations
Provides a full validation framework to your objects.
A minimal implementation could be:
class Person
include ActiveModel::Validations
attr_accessor :first_name, :last_name
validates_each :first_name, :last_name do |record, attr, value|
record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
end
end
Which provides you with the full standard validation stack that you
know from Active Record:
person = Person.new
person.valid? # => true
person.invalid? # => false
person.first_name = 'zoolander'
person.valid? # => false
person.invalid? # => true
person.errors.messages # => {first_name:["starts with z."]}
Note that ActiveModel::Validations automatically adds an errors method
to your instances initialized with a new ActiveModel::Errors object,
so there is no need for you to do this manually.

ActiveRecord - replace model validation error with warning

I want to be able to replace a field error with a warning when saving/updating a model in rails. Basically I want to just write a wrapper around the validation methods that'll generate the error, save the model and perhaps be available in a warnings hash (which works just like the errors hash):
class Person < ActiveRecord::Base
# normal validation
validates_presence_of :name
# validation with warning
validates_numericality_of :age,
:only_integer => true,
:warning => true # <-- only warn
end
>>> p = Person.new(:name => 'john', :age => 2.2)
>>> p.save
=> true # <-- able to save to db
>>> p.warnings.map { |field, message| "#{field} - #{message}" }
["age - is not a number"] # <-- have access to warning content
Any idea how I could implement this? I was able to add :warning => false default value to ActiveRecord::Validations::ClassMethods::DEFAULT_VALIDATION_OPTIONS
By extending the module, but I'm looking for some insight on how to implement the rest. Thanks.
The validation_scopes gem uses some nice metaprogramming magic to give you all of the usual functionality of validations and ActiveRecord::Errors objects in contexts other than object.errors.
For example, you can say:
validation_scope :warnings do |s|
s.validates_presence_of :some_attr
end
The above validation will be triggered as usual on object.valid?, but won't block saves to the database on object.save if some_attr is not present. Any associated ActiveRecord::Errors objects will be found in object.warnings.
Validations specified in the usual manner without a scope will still behave as expected, blocking database saves and assigning error objects to object.errors.
The author has a brief description of the gem's development on his blog.
I don't know if it's ready for Rails 3, but this plugin does what you are looking for:
http://softvalidations.rubyforge.org/
Edited to add:
To update the basic functionality of this with ActiveModel I came up with the following:
#/config/initializer/soft_validate.rb:
module ActiveRecord
class Base
def warnings
#warnings ||= ActiveModel::Errors.new(self)
end
def complete?
warnings.clear
valid?
warnings.empty?
end
end
end
#/lib/soft_validate_validator.rb
class SoftValidateValidator < ActiveModel::EachValidator
def validate(record)
record.warnings.add_on_blank(attributes, options)
end
end
It adds a new Errors like object called warnings and a helper method complete?, and you can add it to a model like so:
class FollowupReport < ActiveRecord::Base
validates :suggestions, :soft_validate => true
end
I made my own gem to solve the problem for Rails 4.1+: https://github.com/s12chung/active_warnings
class BasicModel
include ActiveWarnings
attr_accessor :name
def initialize(name); #name = name; end
warnings do
validates :name, absence: true
end
end
model = BasicModel.new("some_name")
model.safe? # .invalid? equivalent, but for warnings
model.warnings # .errors equivalent

Resources