I hope the title is not too unclear.
I am making arails app and I have a question about rails validation. Consider this code in the User,rb model file:
validates :name,
presence: true,
length: { maximum: 50 },
uniqueness: { case_sensitive: false }
I am using the friendly_id gem to generate slugs for the users. I wont allow users to change their name. What I now need is to ensure that Names are unique in such a way that there will be no UUID's appended to slugs if two people have the same name converted in ascii approximation.
Current behaviour is:
User 1 signs up with a name and gets a slug like this:
name: "Jaiel" slug: "jaiel"
User 2 now does the same name but a bit different:
name: "Jàìèl" slug: "jaiel-6558c3f1-e6a1-4199-a53e-4ccc565657d4"
The problem here as you see I want such a uniqueness validation that User 2 would have been rejected because both names would generate the slug "jaiel" for their friendly_id's
I would appreciate your help on that matter
Thanks
Take a look into ActiveSupport::Inflector.transliterate:
ActiveSupport::Inflector.transliterate('Ærøskøbing')
#=> "AEroskobing"
Also, with this type of validation you might want to go with custom validation (alongside the one you already have):
class User
validate :unique_slug
private
def unique_slug
names = self.class.all.map(&:asci_name)
raise ActiveRecord::RecordInvalid.new(self) if names.include?(asci_name)
end
def asci_name
ActiveSupport::Inflector.transliterate(name)
end
end
Obviously, this is super inefficient to query whole table on each validation, but this is just to point you into one of the possible directions.
Another option would be going for a callback. Transliterating the name upon creation:
before_validation: :transliterate_name
def transliterate_name
self.name = ActiveSupport::Inflector.transliterate(name)
end
It will first transliterate the name, then validate uniqueness of already transliterated name with the validation you have. Looks like a solution, definitely not as heavy as initial one.
Related
I have a User model with an email attribute. Various parts of my app conceive of an "email" differently; sometimes as a string, sometimes as a hash ({ token: 'foo', host: 'bar.com' }), sometimes as an object. This is bad; I want the concept of an email to be consistent wherever I use it.
So, I use an Email object that does what I want. I don't see any good reason to create an Email table; instead, I just want to create a new Email object corresponding to an email string whenever I need one. Therefore User looks like this:
class User < ActiveRecord::Base
def email
Email.new(read_attribute :email)
end
def email= email
write_attribute :email, email.to_s
end
end
However, this causes at least two issues:
I can't search for a user by email without an explicit call to to_s.
I can't run a uniqueness validation on the email column anymore. I get a TypeError: can't cast Email to string. (I can fix this with a custom validator.)
Questions:
Is there something wrong with this approach? The fact that it breaks my validation is a code smell to me.
Is there some way to get the existing validates :email, uniqueness: { case_sensitive: false } validation to work with these new accessor definitions?
We have user records that have an attribute called first_name. Many of these records do no have the first_name attribute filled out and thus it equals nil. We want to introduce a presence validation on this attribute. However we've come across a huge problem. If a user updates their record during any request, that request will fail. This leads to a rather abrasive error that we don't know how to handle.
One solution is to only call the validation when the user is creating a record. This works great but we want to enforce this validation when they are on the profile page and they are attempting to update their profile.
Is there a better way to handle this where we can enforce first name requirements on the update page yet still allow users to update their record without ?
Introducing validations on existing data that does not satisfy the new requirements can be problematic. This concept you're after is fundamentally migration-on-write: You've introduce a data migration that happens over time as records are written to, because the migration cannot occur without individual user input. This is one technique for migrating very large data set in zero-downtime environments, or for forcing password resets on users.
Fundamentally, you need to define the conditions in which validation must happen and find a way to test records (on create or update) for that condition. Your condition should select all new records, plus the records being updated in the context where migration is possible.
Once you've defined the condition, you can modify your validation thusly:
validates :first_name, presence: true, if: -> { condition_for_migration }
Ideally the condition should be some field or combination of fields already present in your table that correctly identifies records as ready to be migrated, but this isn't always possible.
Failing that, you could introduce a field specifically for this purpose. You might call it version_number, set all existing records to 1, and then make the default for all new records 2. Your migration might look like this:
# All existing records will have their `version_number` set to the default of 1
add_column :users, :version_number: :integer, null: false, default: 1
# Change the default to 2 for any records created after this point
change_column_default :users, :version_number, 2
You can then use version_number to tell whether validation should take place:
validates :first_name, presence: true, if: -> { version_number >= 2 }
The key is to make sure that, in the context of your profile form, you also update version_number to enable the validation of first_name:
# app/viws/users/edit.html.haml
= form_for #user do |f|
= f.hidden_field :version, value: 2
= f.input :first_name
In the absence of a real database field for this purpose, you can add a temporary one to your model, which maintains the context only for the lifetime of a particular model instance:
Add an accessor to your model, ie update_from_profile_page
Include that field in the contexts in which you want to require validation
Validate first_name during the creation of any new record
Validate first_name during any update where update_from_profile_page is true
For example:
app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :update_from_profile_page
validates :first_name, presence: true, on: :create
validates :first_name, presence: true, on: :update, if: -> { update_from_profile_page }
end
app/views/user/edit.html.haml (your profile page)
= form_for #user do |f|
= f.input :first_name
app/controllers/users_controller.rb
def update
#user = User.find(params[:id])
#user = update_from_profile_page = true
#user.update(params.require(:user).permit(:first_name)
end
This is less desirable than finding a concrete business-logic-based reason for conditional validation as it involves introducing a virtual field to your model that has no functional value outside of a single specific case of a form submission.
I have a Company model and an Employer model. Employer belongs_to :company and Company has_many :employers. Within my Employer model I have the following validation:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
I'm running into a problem where the above validation fails. Here is an example setup in a controller action that will cause the validation to fail:
company = Company.new(company_params)
# company_params contains nested attributes for employers
company.employers.each do |employer|
employer.password = SecureRandom.hex
end
company.employers.first.role = 'Admin' if client.employers.count == 1
company.save!
admin = company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update(some_attr: 'some_val')
On the last line in the example code snippet, admin.update will fail because the validation is checking to see if company_id is included in the list, which it is not, since the list was generated before company was saved.
Obviously there are ways around this such as grabbing the value of company.id and then using it to define admin later, but that seems like a roundabout solution. What I'd like to know is if there is a better way to solve this problem.
Update
Apparently the possible workaround I suggested doesn't even work.
new_company = Company.find(company.id)
admin = new_company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update
# Fails validation as before
I'm not sure I understand your question completely, but there is an issue in this part of the code:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
The validation is configured on the class-level, so it won't work well with updates on that model (won't be re-evaluated on subsequent validations).
The docs state that you can use a block for inclusion in, so you could try to do that as well:
validates :company_id, inclusion: {in: ->() { Company.pluck(:id).prepend(nil) }}
Some people would recommend that you not even do this validation, but instead, have a database constraint on that column.
I believe you are misusing the inclusion validator here. If you want to validate that an associated model exists, instead of its id column having a value, you can do this in two ways. In ActivRecord, you can use a presence validator.
validates :company, presence: true
You should also use a foreign key constraint on the database level. This prevents a model from being saved if there is no corresponding record in the associated table.
add_foreign_key :employers, :companies
If it gets past ActiveRecord, the database will throw an error if there is no company record with the given company_id.
validates :username, exclusion: { in: %w(about account root etc..)
I am using the above to disallow users from using reserved usernames but they are still able to use them between underscores (I do allow underscores in usernames)
Is there anyway I can make rails validate the reserved username even if it is before or after an underscore?
Thanks
You could create a method to do your validations for you and use plain old ruby in that. As you can see in the docs here
This would look something like this for you:
validate :my_validation_method
def my_validation_method
errors.add(:username, :exclusion) if some_condition
end
What this does is say that the model needs to be validated with the my_validation_method as well as all your normal other validations. You then manually add the field that is in error (in your case :username) to the errors of the model, thus it fails validation.
Also note the validate rather than validates.
Your other question is basically how to check whether an entered value includes some words. You could do this like so:
def my_validation_method
forbid = %w(luke darth artoo fry bender)
errors.add(:username, :exclusion) if forbid.find { |w| username.include?(w) }
end
Here I added a condition to the adding of the error where we loop through each word in the forbidden list and check if username includes this word. Note that "blablaluke" would fail too! So it is not completely what you'd want. But you can play around with this yourself of course.
The level of normalization you do (e.g. stripping away other characters) could give you more control, like preventing ad_min, etc.
Update:
You can strip away characters like so:
username.tr('-_+$^&', '')
You can add whatever you want to strip away to that first string in tr.
according to the rails docs this way you can only check if a value is in a set of given values.
This helper validates that the attributes' values are not included in a given set.
i would just write a custom validation method - you can do whatever you want in there.
I'd probably use a Regex. Like this one:
validates_format_of :username, :with => /\A(([ _]*)(?!(about|admin|root|etc)[ _])[^ _]+)+([ _]+|\z)\z/
How can I skip a specific model validation when importing data?
For example, suppose I have this model:
class Account
validates :street_address, presence: true
end
Normally, I don't want accounts to be saved without addresses, but I'm also going to convert a lot of data from an old system, and many accounts there don't have addresses.
My goal is that I can add the old accounts to the new database, but in the future, when these accounts are edited, a street address will have to be added.
Clarification
As I said, I want to skip a specific validation; others should still run. For example, an account without an account number shouldn't be loaded into the new system at all.
This should work:
class Account
attr_accessor :importing
validates :street_address, presence: true,
unless: Proc.new { |account| account.importing }
end
old_system_accounts.each do |account|
# In the conversion script...
new_account = Account.new
new_account.importing = true # So it knows to ignore that validation
# ... load data from old system
new_account.save!
end
If you're only going to do the conversion one time (i.e, after importing the old data you won't need to do this again), you could just skip validations when you save the imported records instead of modifying your app to support it.
new_account.save validate: false
note that
account.update_attribute(:street_address, new_address)
will skip validations as well. #update_attributes (notice the 's') run validations, where update_attribute (singular) does not.