I have a Game model which has_many Rounds which has_many Shots.
Per game, each cup hit with a shot should be unique. This is easy enough to do with validates_uniqueness_of :cup using a scope of :game_id.
However, how do I validate that each Shot is an increment of +1 of the last shot? I cannot have users select their first shot as having made cup 4. This would make no sense.
My form is using form_for #round which accepts nested attributes for exactly 6 shots.
How do I implement this validation? Do I need to refactor my view or completely rethink this?
Since you are using Rails 3, you get some nice options here. I'm not sure that I understand your problem completely, but I'm assuming that you want some type of validation where the score starts at 1 and increments each time.
Here's a test.
require 'test_helper'
class ShotTest < ActiveSupport::TestCase
test "score validations by game" do
Shot.delete_all # Just to be sure. In a real test setup I would have handled this elsewhere.
shot = Shot.new(:game_id => 1, :score => 1)
assert shot.valid?
shot.save!
assert ! Shot.new(:game_id => 1, :score => 1).valid?
assert ! Shot.new(:game_id => 1, :score => 3).valid?
assert Shot.new(:game_id => 1, :score => 2).valid?
assert Shot.new(:game_id => 2, :score => 1).valid?
end
end
And an example model.
# Stick this in a lib file somewhere
class IncrementValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << "must increment score by +1 " unless value == (Shot.maximum(:score, :conditions => {:game_id => record.game_id} ).to_i + 1)
end
end
class Shot < ActiveRecord::Base
validates :score, :uniqueness => {:scope => :game_id}, :increment => true
end
Test output:
$ ruby -I./test test/unit/shot_test.rb
Loaded suite test/unit/shot_test
Started
.
Finished in 0.042116 seconds.
1 tests, 5 assertions, 0 failures, 0 errors
Related
Does anyone know why the following will not work. Is it my code? I'm trying to call the assign_picks at specific times but I could not get it to work. I then tried the below (in 5 minutes time).
I've been following the instructions in this stackoverflow post
I rolled back the db and then migrated again but nothing happens after 5 minutes. I've tested the assign_picks method by calling it on a button press and it works fine.
Any ideas? I'v been stuck here for hours.
Thanks for reading.
Edit:
I meant to mention that I'm using a virtual machine on Nitrious.io. Would that have anything to do with it?
Models/match.rb
class Match < ActiveRecord::Base
attr_accessible :block, :round, :date, :day, :time, :venue, :team1, :team2, :played, :result, :resultString
has_many :match_picks
has_many :current_blocks
after_create :set_expiry_timer
# register the timer
def set_expiry_timer
delay(:run_at => 5.minutes.from_now).assign_picks
end
def assign_picks
#current_blocks = CurrentBlock.where(:id => 1)
#users = User.where(:curr_block != #current_blocks[0].block)
#matches = Match.where(:block => #current_blocks[0].block)
#users.each do |user|
#matches.each do |match|
MatchPick.create!(:userID => user.id, :matchID => match.id, :blockID => #current_blocks[0].block, :userPick => (0..2).to_a.sample)
end
end
# Increase the current block for application
CurrentBlock.where(:id => 1).each do |cb|
cb.update_attribute(:block, cb.block + 1)
end
end
end
Ok I'm really quite stuck on this and it might be nice to throw around some ideas, this may be a long'ish post.
Firstly let me try and explain what I'm trying to do. Currently I have a model called Book with a nested model called Snippets. I have a column called size in my Book model which defines if it is [0 => 'Short', 1 => 'Medium', 2 => 'Long']. I've also got a total word count in my Book controller which will give me the amount of words in every snippet. Now what I want to try and do is depending on the size [0,1,2] define a different limit on the word count. Example shown below
Size (content length validation) | Word Count
Short (500) per creation | 20,000 words in total
Medium (700) per creation | 50,000 words in total
Long (1000) per creation | 100,000 words in total
current_word_count - total_word_count depending on size [short,med,long]
So depending on the size defined in Book which I have working now I would like the total amount of words for snippets in that book to be defined by model including all current posts so for example if I have a short book and I have 10,000 words already in snippets there should be 10,000 left. The reason I have thought it through this way was because not every user will always post the maximum required.
Now for the code.
First the models:
Book.rb
class Book < ActiveRecord::Base
has_many :snippets
attr_accessible :title, :book_id, :size
def get_word_count
#word_count = 0
self.snippets.each.do |c|
#word_count += c.content.scan(/\w+/).size
end
def short?
size == 0
end
def medium?
size == 1
end
def long?
size == 2
end
end
Snippet.rb
class Snippet < ActiveRecord::Base
before_create :check_limit
belongs_to :book
attr_accessible :content, :book_id
validates :book_id, presence: true
#validates_length_of :content, less_than: 200, if: book.small?
#validates_length_of :content, less_than: 500, if: book.medium?
#validates_length_of :content, less_than: 1000, if: book.long?
def check_limit
if book.word_limit_reached?
errors.add :base, 'Snippet limit reached.'
return false
end
return true
end
end
Seeing as this is a database action I won't really need to touch the controller just yet until I have defined these rules. I've been sat here trying various things but as I'm still new to Rails I'm just trying to do this to get my head around the code and the things you can do.
As always I appreciate your help and feedback.
Ahh, custom validation goodness, this is what I would do:
book.rb
class Book < ActiveRecord::Base
has_many :snippets
def get_word_count
#word_count = []
self.snippets.each do |c|
#word_count << c.content.scan(/\w+/).size
end
#word_count = #word_count.inject(:+)
# Similar to what you had before, but this creates an array, and adds each
# content size to the array, the inject(:+) is a method done on an array to sum
# each element of the array, doesn't work with older versions of ruby, but it's safe
# 1.9.2 and up
end
def short?
size == 0
end
def medium?
size == 1
end
def long?
size == 2
end
end
Snippet.rb
class Snippet < ActiveRecord::Base
belongs_to :book
validate :size_limit
# Here I created a constant containing a hash with your book limitation parameters
# This way you can compare against it easily
BOOK_SIZE = {
0 => {"per" => 500, "total" => 20000},
1 => {"per" => 700, "total" => 50000},
2 => {"per" => 1000, "total" => 100000}
}
def size_limit
# Sets the book size, which is the same as the hash key for your constant BOOK_SIZE
book_limit = self.book.size
# Scans the content attribute and sets the word count
word_count = self.content.scan(/\w+/).size
# This is getting the total word count, for all the snippets and the new snippet
# the user wants to save
current_snippets_size = (self.book.get_word_count || 0) + word_count
# This is where your validation magic happens. There are 2 comparisons done and if they
# don't both pass, add an error that can be passed back to the controller.
# 1st If your content attribute is smaller than allowed for that particular book
# 2nd If your total snippets content for all previous and current snippet are less than
# allowed by the total book size defined in the constant
errors.add(:content, "Content size is too big") unless word_count < BOOK_SIZE[book_limit]['per'] && current_snippets_size < BOOK_SIZE[book_limit]['total']
end
end
so I have these two models:
class Tag < ActiveRecord::Base
has_many :event_tags
attr_accessible :tag_id, :tag_type, :value
end
class EventTag < ActiveRecord::Base
belongs_to :tag
attr_accessible :tag_id, :event_id, :region
end
and this table for Tags:
**tag_id** **tag_type** **value**
1 "funLevel" "Boring..."
2 "funLevel" "A Little"
3 "funLevel" "Hellz ya"
4 "generic" "Needs less clowns"
5 "generic" "Lazer Tag"
...
What I would like to do is write a custom validation where it checks to see:
Each event_id has only one tag_type of "funLevel" attached to it, but can have more than one "generic" tags
For example:
t1 = EventTag.new(:tag_id => 1, :event_id =>777, :region => 'US')
t1.save # success
t2 = EventTag.new(:tag_id => 2, :event_id =>777, :region => 'US')
t2.save # failure
# because (event_id: 777) already has a tag_type of
# "funLevel" associated with it
t3 = EventTag.new(:tag_id => 4, :event_id =>777, :region => 'US')
t3.save # success, because as (tag_id:4) is not "funLevel" type
I have come up with one ugly solution:
def cannot_have_multiple_funLevel_tag
list_of_tag_ids = EventTag.where("event_id = ?", event_id).pluck(:tag_id)
if(Tag.where("tag_id in ?", list_of_tag_ids).pluck(:tag_type).include? "funLevel")
errors.add(:tag_id, "Already has a Fun Level Tag!")
end
Being new to rails, is there a more better/more elegant/more inexpensive way?
The way you have your data structured means that the inbuilt Rails validations are probably not going to be a heap of help to you. If the funLevel attribute was directly accessible by the EventTag class, you could just use something like:
# event_tag.rb
validate :tag_type, uniqueness: { scope: :event_id },
if: Proc.new { |tag| tag.tag_type == "funLevel" }
(unfortunately, from a quick test you don't seem to be able to validate the uniqueness of a virtual attribute.)
Without that, you're probably stuck using a custom validation. The obvious improvement to the custom validation you have (given it looks like you want to have the validation on EventTag) would be to not run the validation unless that EventTag is a funLevel tag:
def cannot_have_multiple_funLevel_tag
return unless self.tag.tag_type == "funLevel"
...
end
** update **
it all seems to be related to a custom validator: if I remove it, it works as expected. see code at the end
**
I have a model budget that has many multi_year_impacts
in the console, if I run:
b = Budget.find(4)
b.multi_year_impacts.size #=> 2
b.update_attributes({multi_year_impacts_attributes: {id: 20, _destroy: true} } ) #=> true
b.multi_year_impacts.size #=> 1 (so far so good)
b.reload
b.multi_year_impacts.size #=> 2 What???
and if before b.reload I do b.save (which shouldn't be needed anyway), it's the same.
Any idea why my child record doesn't get destroyed?
Some additional information, just in case:
Rails 3.2.12
in budget.rb
attr_accessible :multi_year_impacts_attributes
has_many :multi_year_impacts, as: :impactable, :dependent => :destroy
accepts_nested_attributes_for :multi_year_impacts, :allow_destroy => true
validates_with MultiYearImpactValidator # problem seems to com from here
in multi_year_impact.rb
belongs_to :impactable, polymorphic: true
in multi_year_impact_validator.rb
class MultiYearImpactValidator < ActiveModel::Validator
def validate(record)
return false unless record.amount_before && record.amount_after && record.savings
lines = record.multi_year_impacts.delete_if{|x| x.marked_for_destruction?}
%w[amount_before amount_after savings].each do |val|
if lines.inject(0){|s,e| s + e.send(val).to_f} != record.send(val)
record.errors.add(val.to_sym, " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts")
end
end
end
end
it might depend on your rails version, however, comparing your code to the current docs:
Now, when you add the _destroy key to the attributes hash, with a
value that evaluates to true, you will destroy the associated model:
member.avatar_attributes = { :id => '2', :_destroy => '1' }
member.avatar.marked_for_destruction? # => true
member.save
member.reload.avatar # => nil
Note that the model will not be destroyed until the parent is saved.
you could try with:
b.multi_year_impacts_attributes = {id: 20, _destroy: true}
b.save
So it looks like the culprit was here
if lines.inject(0){|s,e| s + e.send(val).to_f} != record.send(val)
record.errors.add(val.to_sym, " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts")
end
changing this to the slightly more complex
total = 0
lines.each do |l|
total += l.send(val).to_f unless l.marked_for_destruction?
end
if total != record.send(val)
record.errors[:amount_before] << " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts"
end
solved the problem.
I use this validation:
validates_numericality_of :price, :greater_than_or_equal_to => 0, :less_than => 1000000
How could I set a different :message for each one of the following cases ?
price < 0
price >= 1000000
Assuming you're using Rails 3, another option you have is to create a custom validator:
# You can put this in lib/better_numericality_validator.rb
class BetterNumericalityValidator < ActiveModel::EachValidator
def validate_each(record,attribute,value)
if value < 0
record.errors[attribute] << "must be greater than or equal to 0"
elsif value >= 1000000
record.errors[attribute] << "must be less than 1000000")
end
end
end
Then you can use your custom validator in your model:
# In your model.rb
validates :price, :better_numericality => true
This method is very similar to Anubhaw's answer. But pulling the logic out into the a custom validator makes it so that you can reuse the validation elsewhere easily, you can easily unit test the validator in isolation, and I personally think that validates :price, :better_numericality => true leaves your model looking cleaner than the alternative.
You can use following in model.rb:-
def validate
if self.price < 0
errors.add(:price, "custom message")
elsif self.price > 1000000
errors.add(:price, "custom message")
end
end
Thanks....
How about:
validates_numericality_of :price, :greater_than_or_equal_to => 0, :message => "Foo"
validates_numericality_of :price, :less_than => 1000000, :message => "Bar"
I've not tested it, but it should work?
Alternatively, Anubhaw's question is a good fallback.
At some point, you should probably ask yourself whether it isn't time to apply some convention over configuration.
In my opinion, an error message such as "Please enter a valid price greater than 0 and less than 1 million" (or similar) is a perfectly valid solution to the problem. It prevents you from adding unnecessary complexity to your application and allows you to move on to other (presumably more important) features.