I have a validator class that i am writing that has three validations, that are run when calling MyVariableName.valid?
validates_length_of :id_number, :is => 13, :message => "A SA ID has to be 13 digits long"
validates_format_of :id_number, :with => /^[0-9]+$/, :message => "A SA ID cannot have any symbols or letters"
validate :sa_id_validator
The third one is a custom validator. The thing is that my validator sa_id_validator requires that the data that is passed in is a 13 digit number, or I will get errors. How can I make sure that the validate :sa_id_validator is only considered after the first two have run?
Sorry if this is a very simple question I have tried figuring this out all of yesterday afternoon.
Note: this validator has to run over a couple thousand entries and is also run on a spreadsheet upload so I need it to be fast..
I saw a way of doing this but it potentially runs the validations twice, which in my case would be bad.
EDIT:
my custom validator looks like this
def sa_id_validator
#note this is specific to South African id's
id_makeup = /(\d{6})(\d{4})(\d{1})(\d{1})(\d{1})/.match(#id_number)
birthdate = /(\d{2})(\d{2})(\d{2})/.match(id_makeup[1])
citizenship = id_makeup[3]
variable = id_makeup[4]
validator_key = id_makeup[5]
birthdate_validator(birthdate) && citizenship_validator(citizenship) && variable_validator(variable) && id_algorithm(id_makeup[0], validator_key)
end
private
def birthdate_validator(birthdate)
Date.valid_date?(birthdate[1].to_i,birthdate[2].to_i,birthdate[3].to_i)
end
def citizenship_validator(citizenship)
/[0]|[1]/.match(citizenship)
end
def variable_validator(variable)
/[8]|[9]/.match(variable)
end
def id_algorithm(id_num, validator_key)
odd_numbers = digits_at_odd_positions
even_numbers = digits_at_even_positions
# step1: the sum off all the digits in odd positions excluding the last digit.
odd_numbers.pop
a = odd_numbers.inject {|sum, x| sum + x}
# step2: concate all the digits in the even positions.
b = even_numbers.join.to_i
# step3: multiply step2 by 2 then add all the numbers in the result together
b_multiplied = (b*2)
b_multiplied_array = b_multiplied.to_s.split('')
int_array = b_multiplied_array.collect{|i| i.to_i}
c = int_array.inject {|sum, x| sum + x}
# step4: add the result from step 1 and 3 together
d = a + c
# step5: the last digit of the id must equal the result of step 4 mod 10, subtracted from 10
return false unless
validator_key == 10 - (d % 10)
end
def digits_at_odd_positions
id_num_as_array.values_at(*id_num_as_array.each_index.select(&:even?))
end
def digits_at_even_positions
id_num_as_array.values_at(*id_num_as_array.each_index.select(&:odd?))
end
def id_num_as_array
id_number.split('').map(&:to_i)
end
end
if i add the :calculations_ok => true attribute to my validation, and then pass in a 12 digit number instead i get this error:
i.valid?
NoMethodError: undefined method `[]' for nil:NilClass
from /home/ruberto/work/toolkit_3/toolkit/lib/id_validator.rb:17:in `sa_id_validator'
so you can see its getting to the custom validation even though it should have failed the validates_length_of :id_number??
I am not quite sure but i have read at some blog that Rails always runs all validations even if the first one is invalid.
What you can do is to make your custom method in such a way that it would become flexible or bouncy in such a way that i would handle all the cases.
This answer would definitely help you.
Hope it would answer your question
Related
I'm using Ruby 3.0.2 and trying to validate using ActiveRecord.
I want to validate user input that is a string but want treat the parts of the input as a numbers so that I can apply some logic to it.
The input needs to look something like this
21-10
I will use regex to ensure that there is a '-' between the two numbers, but I want to be able to validate the numbers to ensure that:
One of the two sets of numbers is 21
Both numbers cannot be larger than 21 unless the difference between them is two apart from each other
My challenge is being able to treat parts of a string as numbers with validation.
An example of input that would be valid/invalid:
11-21 // valid
10-23 // invalid because 23 > 21
11-11 // invalid because neither numbers are 21
22-24 // valid because 22 & 24 within 2 of each other
Any help appreciated!
That can be done with a custom validation method like this:
# in the model
validate :range_string_has_validate_pattern
private
def range_string_has validate_pattern
parts = range_string.split('-') # assuming the attribute is named `range_string`
if parts.size != 2
errors.add(:range_string, "does not contain two parts separated by a '-'")
return
end
begin
parts[0] = Integer(parts[0])
parts[1] = Integer(parts[1])
rescue
errors.add(:range_string, "does not contain two numbers")
return
end
return if parts[0] == 21 || parts[1] == 21
return if (parts[0] - parts[1]).abs == 2
errors.add(:range_string, "does not match requirements")
end
I would go with pattern matching (yay for modern ruby) and a custom validator method
validates :your_attribute, format: { with: /\A\d+-\d+\z/ }
validate :string_format
def string_format
# do not attempt if the format is wrong
return if errors.where(:your_attribute).present?
case your_attribute.split('-').map(&:to_i)
in [..20, ..20]
error.add(:your_attribute, 'Both numbers are less than 21')
in [..20, 22..] | [22.., ..20]
error.add(:your_attribute, 'One is less the other is more')
in [22.. => x, 22.. => y] if y - x > 2 or x - y > 2
error.add(:your_attribute, 'Difference is too big')
else
return
end
end
I want to check if the variable which is basically a user input is a 10 digit phone number or not.
There are 2 sets of validations:
- If num is less than 10 digit then prompt a msg
- if num is a string instead rather than integer
#phone = params[:phone_num]
puts "phone_num: #{#phone}"
if #phone.is_a? Integer
puts "phone_num is int"
if #phone.to_s.length == 10
puts "10 digit"
perform(#phone)
#output = "Valid Number, will receive a call"
end
else
puts "Wont be calling"
#output = "The number is invalid"
end
The output that I get is always The number is invalid no matter what I enter in text box. There are many stack overflow answering dealing with different questions but wondering why my code didn't work.
There is standard validation (length) & (numericality) for this:
#app/models/user.rb
class User < ActiveRecord::Base
validates :phone_num, length: { is: 10 }, numericality: { only_integer: true }
end
This type of validation belongs in the model.
Notes
Your controller will look as follows:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def create
#user = User.new user_params
#user.save #-> validations handled by model
end
end
There's a principle called fat model, skinny controller - you should put "data" logic in your model.
The reason for this is to remove inefficient code from the controller.
It gives you the ability to delegate much of your logic to the Rails core helpers (validations for example), instead of calling your own mass of code in the front-end (like you're doing).
Each time you run a Rails app, the various classes (controller & model) are loaded into memory. Along with all of the Rails classes (ActiveRecord etc), your controllers & models have to be loaded, too.
Any extra code causes causes bloat, making your application buggy & unusable. The best developers know when to use their own code, and when to delegate to Rails. This example is a perfect demonstration of when to delegate.
The output that I get is always The number is invalid no matter what I
enter in text box.
The reason why your code always falls back to else part because the values that are coming from the params will always be strings. So the value of params[:phone_num] is a string. So your code is failing here if #phone.is_a? Integer. Instead you need change it to params[:phone_num].to_i
#phone = params[:phone_num].to_i
puts "phone_num: #{#phone}"
if #phone.is_a? Integer
puts "phone_num is int"
if #phone.to_s.length == 10
puts "10 digit"
perform(#phone)
#output = "Valid Number, will receive a call"
end
else
puts "Wont be calling"
#output = "The number is invalid"
end
Note:
Yes. This is poor way to perform validations. I'm just answering the OP's question.
Take a look at this - A comprehensive regex for phone number validation - how to determine a string looks like a phone number. There's a very complex regex, because people have various forms for entering phone numbers!
I personally don't like super complex regexes, but it's pretty much what they were invented for. So this is when you want to figure out what sorts of forms are acceptable, write some tests, and make your code pass to your acceptance based on the massive link above!
edit: your code is wrong in a bunch of places; params are already a string, so try this! Remember your nested if/else/end, too.
#phone = params[:phone_num]
if #phone =~ /\A\d+\Z/ # replace with better regex
# this just means "string is all numbers"
puts "phone_num is int"
if #phone.length == 10
puts "10 digit"
perform(#phone)
#output = "Valid Number, will receive a call"
else
puts "Number length wrong, #{#phone.length}"
end
else
puts "Wont be calling, not a number: #{#phone.inspect}"
#output = "The number is invalid"
end
This is the stripped down version of my model .
model Paper
PAPER_STARTING_NUMBER = 1
validate_uniqueness_of :number, :allow_blank => true
before_create :alocate_paper_number
def alocate_paper_number
return true if self.number.present?
p_number = Paper.maximum('number') || Paper::PAPER_STARTING_NUMBER
self.number = p_number >= Paper::PAPER_STARTING_NUMBER ? p_number+1 : Paper::PAPER_STARTING_NUMBER
return true
end
end
the problem is I have duplicates in the number column .
Any ideas why and how I can fix this without changing the callback .
I know I could add a uniqueness validation on the database or make a sequence on that column , any other ideas ?
First you have to understand the order of callbacks :
(-) save
(-) valid
(1) before_validation
(-) validate
(2) after_validation
(3) before_save
(4) before_create
(-) create
(5) after_create
(6) after_save
(7) after_commit
So as you can see , it validates the uniquity of your number attribute, and then before_create can at its own disposal go against what your validation wants to accomplish.
In regards to a more cleaner architecture, I would put both of these ideas together in your custom model, as it doesn't seem that the number can be choosen by the User. It's just an incrementer, right?
def alocate_paper_number
p_number = Paper.maximum('number') || Paper::PAPER_STARTING_NUMBER
self.number = p_number + 1
end
That snippet alone, would prevent duplicates, as it always increments upwards ( unless, there's the possibility of the number going the other way that I'm not aware of ), and also there's no reason to return all those trues. Its true enough!
It is in de docs. validate_uniqueness_of TRIES to make it unique. But if two processes add one record at the same time, they both can contain the same number.
If you want to guarantee uniqueness, let the database do it. But because that is different for each DB, Rails does not support it by design.
It's explained here: http://guides.rubyonrails.org/active_record_validations_callbacks.html#uniqueness
With the solution: "To avoid that, you must create a unique index in your database."
How I fixed it ( bare in mind that I couldn't return a validation error )
I've added a uniquness index on the number column ( as mu and Hugo suggested )
and because I couldn't return a validation error in the controller
class PaperController < ApplicationController
def create
begin
#paper.save
rescue ActiveRecord::RecordNotUnique
#paper.number = nil
create
end
end
end
Lets say I have a Collection of users. Is there a way of using mongoid to find n random users in the collection where it does not return the same user twice? For now lets say the user collection looks like this:
class User
include Mongoid::Document
field :name
end
Simple huh?
Thanks
If you just want one document, and don't want to define a new criteria method, you could just do this:
random_model = Model.skip(rand(Model.count)).first
If you want to find a random model based on some criteria:
criteria = Model.scoped_whatever.where(conditions) # query example
random_model = criteria.skip(rand(criteria.count)).first
The best solution is going to depend on the expected size of the collection.
For tiny collections, just get all of them and .shuffle.slice!
For small sizes of n, you can get away with something like this:
result = (0..User.count-1).sort_by{rand}.slice(0, n).collect! do |i| User.skip(i).first end
For large sizes of n, I would recommend creating a "random" column to sort by. See here for details: http://cookbook.mongodb.org/patterns/random-attribute/ https://github.com/mongodb/cookbook/blob/master/content/patterns/random-attribute.txt
MongoDB 3.2 comes to the rescue with $sample (link to doc)
EDIT : The most recent of Mongoid has implemented $sample, so you can call YourCollection.all.sample(5)
Previous versions of mongoid
Mongoid doesn't support sample until Mongoid 6, so you have to run this aggregate query with the Mongo driver :
samples = User.collection.aggregate([ { '$sample': { size: 3 } } ])
# call samples.to_a if you want to get the objects in memory
What you can do with that
I believe the functionnality should make its way soon to Mongoid, but in the meantime
module Utility
module_function
def sample(model, count)
ids = model.collection.aggregate([
{ '$sample': { size: count } }, # Sample from the collection
{ '$project': { _id: 1} } # Keep only ID fields
]).to_a.map(&:values).flatten # Some Ruby magic
model.find(ids)
end
end
Utility.sample(User, 50)
If you really want simplicity you could use this instead:
class Mongoid::Criteria
def random(n = 1)
indexes = (0..self.count-1).sort_by{rand}.slice(0,n).collect!
if n == 1
return self.skip(indexes.first).first
else
return indexes.map{ |index| self.skip(index).first }
end
end
end
module Mongoid
module Finders
def random(n = 1)
criteria.random(n)
end
end
end
You just have to call User.random(5) and you'll get 5 random users.
It'll also work with filtering, so if you want only registered users you can do User.where(:registered => true).random(5).
This will take a while for large collections so I recommend using an alternate method where you would take a random division of the count (e.g.: 25 000 to 30 000) and randomize that range.
You can do this by
generate random offset which will further satisfy to pick the next n
elements (without exceeding the limit)
Assume count is 10, and the n is 5
to do this check the given n is less than the total count
if no set the offset to 0, and go to step 8
if yes, subtract the n from the total count, and you will get a number 5
Use this to find a random number, the number definitely will be from 0 to 5 (Assume 2)
Use the random number 2 as offset
now you can take the random 5 users by simply passing this offset and the n (5) as a limit.
now you get users from 3 to 7
code
>> cnt = User.count
=> 10
>> n = 5
=> 5
>> offset = 0
=> 0
>> if n<cnt
>> offset = rand(cnt-n)
>> end
>> 2
>> User.skip(offset).limit(n)
and you can put this in a method
def get_random_users(n)
offset = 0
cnt = User.count
if n < cnt
offset = rand(cnt-n)
end
User.skip(offset).limit(n)
end
and call it like
rand_users = get_random_users(5)
hope this helps
Since I want to keep a criteria, I do:
scope :random, ->{
random_field_for_ordering = fields.keys.sample
random_direction_to_order = %w(asc desc).sample
order_by([[random_field_for_ordering, random_direction_to_order]])
}
Just encountered such a problem. Tried
Model.all.sample
and it works for me
The approach from #moox is really interesting but I doubt that monkeypatching the whole Mongoid is a good idea here. So my approach is just to write a concern Randomizable that can included in each model you use this feature. This goes to app/models/concerns/randomizeable.rb:
module Randomizable
extend ActiveSupport::Concern
module ClassMethods
def random(n = 1)
indexes = (0..count - 1).sort_by { rand }.slice(0, n).collect!
return skip(indexes.first).first if n == 1
indexes.map { |index| skip(index).first }
end
end
end
Then your User model would look like this:
class User
include Mongoid::Document
include Randomizable
field :name
end
And the tests....
require 'spec_helper'
class RandomizableCollection
include Mongoid::Document
include Randomizable
field :name
end
describe RandomizableCollection do
before do
RandomizableCollection.create name: 'Hans Bratwurst'
RandomizableCollection.create name: 'Werner Salami'
RandomizableCollection.create name: 'Susi Wienerli'
end
it 'returns a random document' do
srand(2)
expect(RandomizableCollection.random(1).name).to eq 'Werner Salami'
end
it 'returns an array of random documents' do
srand(1)
expect(RandomizableCollection.random(2).map &:name).to eq ['Susi Wienerli', 'Hans Bratwurst']
end
end
I think it is better to focus on randomizing the returned result set so I tried:
Model.all.to_a.shuffle
Hope this helps.
I'm trying to remove the commas from a field in a model. I want the user to type a number, i.e. 10,000 and that number should be stored in the database as 10000. I was hoping that I could do some model-side normalization to remove the comma. I don't want to depend on the view or controller to properly format my data.
I tried:
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
end
no worky.
http://github.com/mdeering/attribute_normalizer looks like a promising solution to this common problem. Here are a few examples from the home page:
# By default it will strip leading and trailing whitespace
# and set to nil if blank.
normalize_attributes :author, :publisher
# Using one of our predefined normalizers.
normalize_attribute :price, :with => :currency
# You can also define your normalization block inline.
normalize_attribute :title do |value|
value.is_a?(String) ? value.titleize.strip : value
end
So in your case you might do something like this:
normalize_attribute :title do |value|
value.to_s.gsub(',', '')
end
I think you're doing it right. This test passes:
test "should remove commas from thenumber" do
f = Foo.new(:thenumber => "10,000")
f.save
f = Foo.find(f.id)
assert f.thenumber == "10000"
end
And I used your code.
class Foo < ActiveRecord::Base
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
end
end
Now, my schema is set up for thenumber to be a string though, not an integer.
Started
.
Finished in 0.049666 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
If you wanted to store this in the db as an integer, then you definitely need to override the setter:
def thenumber=(value)
self['thenumber'] = value.to_s.gsub(',','').to_i
end
If you do it your way, with an integer column, it gets truncated by AR....
>> f.thenumber = "10,000"
=> "10,000"
>> f.thenumber
=> 10
That's a little-known thing with Ruby and integers... it auto-casts by truncating anything that's no longer an integer.
irb(main):004:0> i = "155-brian-hogan".to_i
=> 155
Can be cool for things like
/users/155-brian-hogan
#user = User.find_by_id(params[:id])
But not so cool for what you're doing.
So either change the col to a string and use the filter, or change the setter :)
Good luck!
The problem with doing it that way is that for a while, the non-normalized stuff will exist in the object; if you have code that works on the attributes before stuff gets normalised, then that will be a problem.
You could define a setter:
def thenumber=(value)
# normalise stuff here, call write_attribute
end
Unfortunately I think a lot of the Rails form stuff writes the attributes directly, which is one of the reasons I don't tend to use it.
Or you could normalise the params in the controller before you pass them through.
Does ruby let you interchange between a . and [''] ?
I don't know, I'll try later, but I think you are supposed to use .
self.thenumber = self.thenumber.to_s.gsub(',','')
You should return true from your before_validation method, otherwise if the expression being assigned to self['thenumber'] ends up being nil or false, the data will not be saved, per the Rails documention:
If a before_* callback returns false,
all the later callbacks and the
associated action are cancelled.
Ostensibly, you are trying to normalize here then check the result of the normalization with your Rails validations, which will decide if nil/false/blank are okay or not.
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
return true
end