Inheriting Rails i18n validation error messages in the subclass - ruby-on-rails

What I understand
Suppose I have a class with a handy validation like:
User < ActiveRecord::Base
validates :username, :format => {/regex/}, :message => :name_format
end
In this case, I can use i18n to make the error message translatable, by including the following in my /config/locals/en.yml:
en:
activerecord:
errors:
models:
user:
attributes:
username:
name_format: 'has the way-wrong format, bro!'
This is fine and generally really handy.
What I want to know:
My question is: What happens when I have subclasses that inherit from User:
UserSubclassOne < User
# extra stuff
end
UserSubclassTwo < User
# extra stuff
end
...
UserSubclassEnn < User
# extra stuff
end
Now the problem is that Rails can't find the translation user_subclass_one.attributes.username.name_format.
It complains:
translation missing:
en.activerecord.errors.models.user_subclass_one.attributes.username.name_format
I'd hope that Rails would look up the hierarchy of UserSubclassOne to User when searching for a string in en.yml and then notice when it gets a 'hit', but (unless I've done something horribly wrong) apparently that doesn't happen.
An obvious solution is to duplicate the data in en.yml.en.errors.models for user, user_subclass_one, user_subclass_two, etc, but my Rails-sense tells me that this is deeply wrong.
Any ideas, folks?
Potential Complication:
User is defined in a gem MyGem that is included in a Rails engine MyEngine that is included in the full-on Rails app MyApp that defines UserSubclassOne, ..., UserSubclassEnn. I don't think this should matter though, since the validations are running in MyGem::User, which is where the en.yml file lives -- just wanted to let people know in case it does.
Ultimate problem/solution:
So it turns out that the problem was namespacing. Recall that MyApp (which defines UserSubclassOne) uses MyGem (which defines User). It turns out User is actually in the namespace MyGem (this is not necessarily always the case), so the full declaration line at the beginning of User is not:
User < ActiveRecord::Base
but rather
MyGem::User < ActiveRecord::Base
.
When the i18n gem looks up the class hierarchy, it notices this namespace and searches for my_gem/user, rather than simply user, my_gem.user, my_gem: user, etc.
Thus I had to change my en.yml file to:
/config/locals/en.yml:
en:
activerecord:
errors:
models:
my_gem/user:
attributes:
username:
name_format: 'has the way-wrong format, bro!'
and bingo!

So it turns out that the problem was namespacing. Recall that MyApp (which defines UserSubclassOne) uses MyGem (which defines User). It turns out User is actually in the namespace MyGem (this is not necessarily always the case), so the full declaration line at the beginning of User is not:
User < ActiveRecord::Base
but rather
MyGem::User < ActiveRecord::Base
.
When the i18n gem looks up the class hierarchy, it notices this namespace and searches for my_gem/user, rather than simply user, my_gem.user, my_gem: user, etc.
Thus I had to change my en.yml file to:
/config/locals/en.yml:
en:
activerecord:
errors:
models:
my_gem/user:
attributes:
username:
name_format: 'has the way-wrong format, bro!'
and bingo!

According to the Rails Guides for i18n regarding Error Message Scopes (5.1.1) for Active Record validation error messages, what you're attempting to do should work:
Consider a User model with a validation for the name attribute like this:
class User < ActiveRecord::Base
validates :name, :presence => true
end
<...snip...>
When your models are additionally using inheritance then the messages are looked up in the inheritance chain.
For example, you might have an Admin model inheriting from User:
class Admin < User
validates :name, :presence => true
end
Then Active Record will look for messages in this order:
activerecord.errors.models.admin.attributes.name.blank
activerecord.errors.models.admin.blank
activerecord.errors.models.user.attributes.name.blank
activerecord.errors.models.user.blank
activerecord.errors.messages.blank
errors.attributes.name.blank
errors.messages.blank
This way you can provide special translations for various error messages at different points in your models inheritance chain and in the attributes, models, or default scopes.
So, in your case, assuming your classes look something like this:
app/models/user.rb
User < ActiveRecord::Base
validates :username, :format => {/regex/}, :message => :name_format
end
app/models/user_subclass.rb
UserSubclass < User
validates :username, :format => {/regex/}, :message => :name_format
end
and your config/locales/en.yml looks something like:
en:
activerecord:
errors:
models:
user:
attributes:
username:
name_format: 'has the way-wrong format, bro!'
then the message searching for a validation on UserSubClass should go:
activerecord.errors.models.user_sublcass.attributes.username.name_format # fail
activerecord.errors.models.user_sublcass.name_format # fail
activerecord.errors.models.user.attributes.username.name_format # success
activerecord.errors.models.user.name_format
# ...
Assuming that your model files and yaml files look similar to what's above, then the potential complication you mentioned may be the issue, but obviously I can't be certain.

Related

How can I store a regex expression string in a helper method to use to validate several different fields?

I have several different fields that store different phone numbers in my rails app as a string. I can use the built-in rails format validator with a regex expression. Is there a way to place this expression in a helper method for all my phone number validations instead of having to open each model file and paste the expression string.
You can require any file from application.rb:
# application.rb
require Rails.root.join "lib", "regexes.rb"
# lib/regexes.rb
PHONE_NUMBER_REGEX = /your regex/
Then you simply use the constant wherever needed
You can alternatively make use of the built in autoload functionality of Rails, for example with the concern approach the other commenter laid out - the concern file is autoloaded by default, as are models, controllers, etc
Loading custom files instead of using the Rails' defaults might not seem idiomatic or the "rails way". However, I do think it's important to understand that you can load any files you want. Some people autoload the entire lib/ folder and subfolders (see Auto-loading lib files in Rails 4)
Another alternative is to place your code somewhere in the config/initializers folder, these files are automatically loaded at startup and you can define shared classes/modules/constants there
Add a custom validator app/validators/phone_validator.rb
class PhoneValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.to_s =~ /YOUR_REGEX_HERE/
record.errors.add attribute, 'must be a valid phone number'
end
end
end
Then in your models
class MyModel < ApplicationRecord
#phone: true tells it to use the PhoneValidator defined above
validates :phone_number, presence: true, phone: true
end
One way to do this is to create a concern that will be included in each model that has a phone number. In models/concerns, create a new file called something like phonable.rb.
# models/concerns/phonable.rb
module Phonable
extend ActiveSupport::Concern
VALID_PHONE_REGEX = /\A\+?\d+\z/ # Use your regex here
end
Now include the concern like this:
# models/my_model.rb
class MyModel < ApplicationRecord
include Phonable
validates :phone, format: {with: VALID_PHONE_REGEX}
...
end
Now that you have a Phonable concern, you can put any other phone-number-related logic here as well, such as parsing and the like. The advantage of this approach is that all your logic related to phone numbers will be available for use in the models that need it, and none of that logic will be available in models that don't need it.
You can put it in ApplicationRecord (application_record.rb)
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
VALID_PHONE_NUMBER_REGEX = /\d{10}/ # your regex here
end
And then you can use it in any model that inherits fro ApplicationRecord

How do disable email validation in Clearance

I'm trying to get Clearance to work with AWS Dynamo as the back-end store. The problem I'm having is that I can't get Clearance to not do the email-uniqueness validation, which it can't do because it's not able to do standard ActiveRecord uniqueness validations via a SQL query.
According to the comments in the code, I should be able to have my User object return email_optional? true, and that should disable the uniqueness validation on emails. So I have:
class User < ApplicationRecord
include Dynamoid::Document
include Clearance::User
field :name
field :email
def email_optional?
puts 'yes, email is optional'
true
end
end
But, when I try to create a user I get an error, and, more to the point, the puts is not executed:
$ rails c
Running via Spring preloader in process 18665
Loading development environment (Rails 5.1.3)
irb(main):001:0> u = User.new(name: 'ijd', email: 'ian#fu.bar', password: 'test')
ActiveRecord::StatementInvalid: Could not find table 'editor_development_users'
from (irb):1
Update: the reply from #spickermann reminded me that I should have noted that I also tried without subclassing ActiveRecord::Base (via ApplicationRecord). It gives a different error:
class User
include Dynamoid::Document
....
irb(main):002:0> reload!
Reloading...
=> true
irb(main):003:0> u = User.new(name: 'ijd', email: 'ian#fu.bar', password: 'test')
ArgumentError: Unknown validator: 'UniquenessValidator'
from app/models/user.rb:4:in `include'
from app/models/user.rb:4:in `<class:User>'
from app/models/user.rb:2:in `<top (required)>'
from (irb):3
User.new does not trigger validations. Therefore the error cannot be connected to the validations itself.
At the moment your User model is kind of both: A subclass of ActiveRecord::Base and it behaves like a Dynamoid::Document.
class User < ApplicationRecord
include Dynamoid::Document
# ...
end
ActiveRecord::Base reads the table definition from the database when an instance is initialized. This leads to your exception, because the table does not exist. Just remove the inheritance from ApplicationRecord.
class User
include Dynamoid::Document
# ...
end
The second issue when you remove the inheritance is more complex. Usually, I would suggest to just include ActiveModel::Validations when you want to validate models that do not inherit from ActiveRecord::Base. But the UniquenessValidator isn't defined in ActiveModel::Validations but in ActiveRecord::Validations (what makes kind of sense). This makes Clearance incompatible with models that do not inherit from ActiveRecord::Base.
I would probably define a dummy implementation of a UniquenessValidator as a work-around:
class User
include Dynamoid::Document
class UniquenessValidator
def initialize(_options); end
def def validate_each(_record, _attribute, _value); end
end
# ...
end

How can I extend ActiveRecord::Associations to a frozen_record in rails

I'm using the Frozen Record gem in Rails to upload a YAML file containing some fixed questions for my app. Once they're uploaded I want to use a SaleQualifier model to grab each question, associate it with an answer, and then use it as a state machine to move through a question tree.
I've got the Frozen Record gem working and the YAML file uploads just fine. When I tried associating the Model with a new Model (SaleQualifier) I got 'method_missing': undefined method 'belongs_to' for Question:Class (NoMethodError)
As a result I added the include ActiveRecord::Associations components in order to let me associate the new record with my SaleQualifier Question belongs_to :sale_qualifier- but this then throws:
'method_missing': undefined method 'dangerous_attribute_method?' for Question:Class (NoMethodError)
According to my search, this error is thrown when I've already declared the method beforehand. I don't know where this is defined, but from the Frozen_Record gem files I can see they set up a FrozenRecord with the following:
module FrozenRecord
class Base
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::AttributeMethods
include ActiveModel::Serializers::JSON
include ActiveModel::Serializers::Xml
end
end
I'm starting to think using this gem might be overkill, and perhaps I should just load my questions into a normal ActiveRecord::Base model, but I like the FrozenRecord idea, as that's exactly what I'm trying to achieve.
My code:
class Question < FrozenRecord::Base
include ActiveModel::Validations
include ActiveRecord::Associations
validates :next_question_id_yes, :question_text, :answer_type, presence: true
belongs_to :sale_qualifier
self.base_path = 'config/initializers/'
end
SaleQualifier:
class SaleQualifier < ActiveRecord::Base
has_many :questions
end
Can anyone help me unpick the mess I seem to have dug myself into? Maybe I ought to just dig out the YAML upload functions from FrozenRecord and dump them into my Question model without using the gem.
So in the end, Frozen_Record wasn't really adding much to my app it seems. Given the files are loaded from YAML and there's no Controller, I presume there's no way for the Question records to be updated, unless someone has already compromised my DB and inserted their own questions.yml file.
As such I just changed the Question model as follows to load the YAML and insert it into the Database as new Question records:
class Question < ActiveRecord::Base
validates :next_question_id_yes, :question_text, :answer_type, presence: true
belongs_to :sale_qualifier
File.open("#{Rails.root}/config/initializers/questions.yml", 'r') do |file|
YAML::load(file).each do |record|
Question.create(record)
end
end
end

How to properly translate Paperclip error messages?

I am using Ruby on Rails 3.2.2 and the Paperclip plugin. Since I would like to translate Paperclip error messages, I use the following code:
class Article < ActiveRecord::Base
validates_attachment_size :picture,
:less_than => 4.megabytes,
:message => I18n.t('trl_too_big_file_size', :scope => 'activerecord.errors.messages')
end
My .yml files are:
# <APP_PATH>/config/locales/en.yml (english language)
activerecord:
errors:
messages:
trl_too_big_file_size: is too big
# <APP_PATH>/config/locales/it.yml (italian language)
activerecord:
errors:
messages:
trl_too_big_file_size: è troppo grande
My ApplicationController is:
class ApplicationController < ActionController::Base
before_filter :set_locale
def set_locale
# I am using code from http://guides.rubyonrails.org/i18n.html#setting-and-passing-the-locale.
I18n.locale = request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
end
# BTW: My `I18n.default_locale` is `:en` (english).
end
However, when I submit the form related to the Paperclip attachment my application generate a not translated text - that is, it generates (and outputs) a not properly translated string because it refers always to the default locale languages error string en.activerecord.errors.messages.trl_too_big_file_size even if the I18n.locale is :it. I made some research (for example, 1, 2), but I still cannot figure out how to properly translate error messages related to the Paperclip gem in model files (see the code above). It seems a bug of the Parperclip gem (also because that seems that I am not the only one to get this issue)...
How can I solve the issue?
P.S.: I think the problem is related to some "file loading order process" of ApplicationController-Paperclip gem... but I do not know how to solve the problem.
Instead of using a lambda in the message parameter can't you simply use the appropriate key in the YAML file? Specifically, wouldn't something like:
class Article < ActiveRecord::Base
validates_attachment_size :picture,
:less_than => 4.megabytes
end
with an entry in the Italian locale YAML file that looks like
it:
activerecord:
errors:
models:
article:
attributes:
picture:
less_than: è troppo grande
with a similar entry in the English local YAML file
en:
activerecord:
errors:
models:
article:
attributes:
picture:
less_than: is too big
Assuming your locale is getting set correctly, and that other ActiveRecord localized messages are getting displayed correctly, then I'd expect this to work as Paperclip's validators use the underlying ActiveRecord message bundles via the I18n.t method.
you might need to use a lambda for the message?
see https://github.com/thoughtbot/paperclip/pull/411
class Article < ActiveRecord::Base
validates_attachment_size :picture,
:less_than => 4.megabytes,
:message => lambda {
I18n.t('trl_too_big_file_size', :scope => 'activerecord.errors.messages')
}
end

Rails: How to validate data manually using a given model?

I am creating a Tumblr alternative to learn how use Rails. I am at the authentication part, and I decided to do it from scratch. I want to allow users to log in using either their username or their email. The user logs in via the Sessions controller but I need to verify if the login is either a valid username or a valid email. So I need to validate data in the Sessions controller using the User model.
I found this answer on SO: How do I validate a non-model form in Rails 3? but it will force me to duplicate the validations. Is that the only way to do it, or is there another way that is cleaner?
The best option I can imagine is creating a module and then including it at your User model and at the object you're going to use for the form:
module AuthenticationValidation
def self.included( base )
base.validates :username, :presence => true
base.validates :email, :presence => true
# add your other validations here
end
end
And then include this module at your models:
class User < ActiveRecord::Base
include AuthenticationValidation
end
class LoginForm
include ActiveModel::Validations
include ActiveModel::Conversion
include AuthenticationValidation
attr_accessor :username, :email
end
And then you have avoided repeating the validation itself.

Resources