rails - validate product ordered has the available quantity - ruby-on-rails

I have two related validations on separate models. I can validate one way but not another. The post validation works: if I try to change the quantity available as the seller, and people have already ordered some of it, it (correctly) won't let me change the quantity below the amount ordered already.
The order validation does not work: if I try changing the order amount to more than is available, it lets me. It checks only the current sum (I guess before the save), and does not notice the buyer trying to sneak out more than what's available. When I try and change the order back to less than what is available, it won't let me, checking the current sum ordered (before save).
I also tried validating the quantity of the order + the remaining available stock (available -sum ordered), but have the same problem.
How would I get the validation to check what the quantity would be after the save, and then not save it should the value be invalid? It would have to work also for editing an order
class Post < ActiveRecord::Base
belongs_to :product
belongs_to :event
has_many :orders, :dependent => :destroy
attr_accessible :quantity, :deadline, :product_id, :event_id
validate :quantity_gt_ordered
def quantity_gt_ordered
self.errors.add(:quantity, " - People have already ordered more than this") unless self.quantity >= self.sum_orders
end
def sum_orders
self.orders.sum(:quantity)
end
class Order < ActiveRecord::Base
belongs_to :user
belongs_to :post
attr_accessible :order_price, :post_id, :user_id, :quantity
validate :quantity_is_available
def quantity_is_available
self.errors.add(:quantity, " - Please order only what's available") unless self.sum_post_orders <= self.post.quantity
end
def sum_post_orders
Order.where(:post => self.post).sum(:quantity)
end

You should just be able to compare to the available quantity minus the ordered quantity:
available_quantity = self.post.quantity - self.sum_post_order
if quantity > available_quantity
self.errors.add(:quantity, " - Please order only what's available")
end
And also make sure your sum_post_orders doesn't include the current order:
Order.where("id != ?", self.id).where(:post => self.post).sum(:quantity)

Related

Rails accept_nested_attributes still throws "Can't mass-assign protected attributes"

I think I'm either missing something really simple or something really obscure. Hoping someone can spot it for me or explain my muppetry.
Ok, So there are two models, Basket and BasketItem.
I've set Basket to accept_nested_attributes :basket_items with the intention of using fields_for in an edit view of Basket.
However when run up it still screams that
Error: Can't mass-assign protected attributes: basket_items_attributes
For the sake of this question I've boiled down to the same issue if I do a manual basket.update_attributes in the console with just one or two basket_item attributes. So I know it's a model issue, not a view or controller issue.
e.g.:
basket.update_attributes("basket_items_attributes"=>[{"qty"=>"1", "id"=>"29"}, {"qty"=>"7", "id"=>"30"}])
or similarly with a hash more like fields_for makes
basket.update_attributes( "basket_items_attributes"=>{
"0"=>{"qty"=>"1", "id"=>"29"},
"1"=>{"qty"=>"7", "id"=>"30"}
})
I've ensured that the associates in defined before the accepts_nested_attibutes_for, that the child model has the appropriate attributes accesable too, tried removing additional attributes for the nested data, lots of fiddling to no avail.
basket.rb
class Basket < ActiveRecord::Base
has_many :basket_items
attr_accessible :user_id
accepts_nested_attributes_for :basket_items
belongs_to :user
def total
total = 0
basket_items.each do |line_item|
total += line_item.total
end
return total
end
# Add new Variant or increment existing Item with new Quantity
def add_variant(variant_id = nil, qty = 0)
variant = Variant.find(variant_id)
# Find if already listed
basket_item = basket_items.find(:first, :conditions => {:variant_id => variant.id})
if (basket_item.nil?) then
basket_item = basket_items.new(:variant => variant, :qty => qty)
else
basket_item.qty += qty
end
basket_item.save
end
end
basket_item.rb
class BasketItem < ActiveRecord::Base
belongs_to :basket
belongs_to :variant
attr_accessible :id, :qty, :variant, :basket_id
def price
variant.price
end
def sku
return variant.sku
end
def description
variant.short_description
end
def total
price * qty
end
end
As the error says, you just need to add basket_items_attributes to your list of accepted attributes.
So you'd have
attr_accessible :user_id, :basket_items_attributes
at the top of your basket.rb file

Rails: How do I validate model based on parent's attributes?

I have three models named Metric, Entry, and Measurement:
class Metric < ActiveRecord::Base
has_many :entries
attr_accessible :name, :required_measurements, :optional_measurements
end
class Entry < ActiveRecord::Base
belongs_to :metric
has_many :measurements
attr_accessible :metric_id
end
class Measurement < ActiveRecord::Base
belongs_to :entry
attr_accessible :name, :value
end
They are associated, and I can create nested instances of each this way:
bp = Metric.create(:name => "Blood Pressure", :required_measurement_names => ["Diastolic", "Systolic"])
bp_entry = bp.entries.create()
bp_entry.measurements.create({:name => "Systolic", :value =>140}, {:name => "Diastolic", :value =>90})
How do I validate the measurements for blood pressure based on the :required_measurement_names attribute in the Metric model? For example, how would I ensure that only "Systolic" and "Diastolic" are entered as measurements?
Is there a better way to about setting up these associations and validations?
Thanks!
Looks like entry is a join model between Measurement and Metric. so it makes sense for your validation to go there.
To ensure that all metrics required metrics are covered
def validate_required_measurements
metric.required_measurements.each{ |requirement|
unless (measurements.map{|measurement| measurement.name}.includes requirement)
errors.add_to_base "required measurement #{requirement} is missing"
end
}
end
To ensure that only accepted metrics are included (assuming optional metrics are accepted too).
def validate_accepted_measurements
measurements.each{ |measurement|
unless ((metric.required_measurements + metric.optional_measurements).include measurement.name )
errors.add_to_base "invalid measurement #{measurement.name} for metric"
end
}
end
Putting it all together add the above and following to the Entry model
validate: validate_accepted_measurements, :validate_required_measurements
N.B. my Ruby is a little rusty so this isn't guaranteed to work after copying and pasting, but it should get you close enough to fix the syntax errors that slipped through.

:counter_cache for total items

I hava a simple set of two related tables of an 'order' that has many 'line_items'. There is also a quantity associated to a line item, e.g.
Order1
line_item a: 'basket weaving for beginners', quantity: 3
line_item b: 'a dummies guide to vampirism', quantity: 1
When I establish the migration I can include the quantity using:
Order.find(:all).each do |o|
o.update_attribute :line_items_count, o.line_items.map(&:quantity).sum
end
which gives me the correct number of items (4), but I don't appear to be able to do it on the Order model because I'm unable to pass in the quantity of line items, and so it just counts the number of line items (2).
So in the line_item model I have:
belongs_to :order, :counter_cache => true
Is there any way I can specify the quantity so that it correctly says 4 instead of 2?
The 'counter_cache` feature to meant to maintain the count(not the sum) of dependent items.
You can easily achieve this by writing few lines of ruby code.
Let us assume that you have a column called line_items_sum in your orders table. The value of this column should default to 0.
class AddLineItemsSumToOrder < ActiveRecord::Migration
def self.up
add_column :orders, :line_items_sum, :integer, :default => 0
end
def self.down
remove_column :orders, :line_items_sum
end
end
class Order < ActiveRecord::Base
has_many :line_items
end
Now add the callback to the LineItem class.
class LineItem < ActiveRecord::Base
validates_numericality_of :quantity
belongs_to :order
after_save :update_line_items_sum
private
def update_line_items_sum
return true unless quantity_changed?
Order.update_counters order.id,
:line_items_sum => (quantity - (quantity_was || 0))
return true
end
end
I think your best bet would be to write your own method for caching the total quantity. If you don't follow the "Rails way" for keeping the counter, you're better off writing your own.

Rails: Getting random product within several categories

I have a question about random entries in Rails 3.
I have two models:
class Product < ActiveRecord::Base
belongs_to :category
self.random
Product.find :first, :offset => ( Product.count * ActiveSupport::SecureRandom.random_number ).to_i
end
end
class Category < ActiveRecord::Base
has_many :products
end
I'm able to get a random product within all products using an random offset castet to int. But I want also be able to get random products WITHIN several given categories. I tried something like this, but this doesn't work, because of the offset index:
class Product < ActiveRecord::Base
belongs_to :category
self.random cat=["Mac", "Windows"]
joins(:categories).where(:categories => { :name => cat }).where(:first, :offset => ( Product.count * ActiveSupport::SecureRandom.random_number ).to_i)
end
end
Anybody here who knows a better solution?
thx!
tux
You could try and simplify this a little:
class Product < ActiveRecord::Base
def self.random(cat = nil)
cat ||= %w[ Mac Windows ]
joins(:categories).where(:categories => { :name => cat }).offset(ActiveSupport::SecureRandom.random_number(self.count)).first
end
end
Rails 3 has a lot of convenient helper methods like offset and first that can reduce the number of arguments you need to pass to the where clause.
If you're having issues with the join, where the number of products that match is smaller than the number of products in total, you need to use ORDER BY RAND() instead. It's actually not that huge a deal performance wise in most cases and you can always benchmark to make sure it works for you.
the offset happens after the order, so you can add .order('rand()') then you will be getting random elements from multiple categories. but since the order is random you also do not need offset anymore. so just:
class Product < ActiveRecord::Base
belongs_to :category
self.random cat=["Mac", "Windows"]
joins(:categories).where(:categories => { :name => cat }).order('rand()').first
end
end

Ruby on Rails: How do I sum up these elements in my database?

I have a video_votes table with all the votes with a column called value set to 1 or -1. I want to sum up all the values of the video's votes and display that net vote count. First, how should I sum this up, and second, should I store this value in my video table? If so, how?
I would start with this until performance became an issue:
class Video < AR::Base
has_many :video_votes
def vote_sum
video_votes.sum(:value)
end
end
class VideoVote < AR::Base
belongs_to :video
validates_inclusion_of :value, :in => [-1,1]
end
Once performance became an issue and I wanted to cache the summed value I might do something like this:
class Video < AR::Base
has_many :video_votes
# Override vote_sum attribute to get the db count if not stored in the db yet.
# The alternative is that you could remove this method and have the field
# populated by a migration.
def vote_sum
read_attribute(:vote_sum) || video_votes.sum(:value)
end
end
class VideoVote < AR::Base
belongs_to :video
validates_inclusion_of :value, :in => [-1,1]
after_create :update_video_vote_sum
private
def update_video_vote_sum
video.update_attributes(:vote_sum => video.vote_sum + value)
end
end
Check out the AR documentation on "Overwriting default accessors" (scroll down a bit)
http://ar.rubyonrails.org/classes/ActiveRecord/Base.html
In your Video model:
def total_votes
self.votes.sum(:value)
end
So an example might be:
#video.total_votes
Use ActiveRecord's sum method.
VideoVote.sum('value')
You shouldn't store it in the same table. If you have other fields you want to summarize then create a "summary" table and periodically summarize the fields and store the values there. ActiveRecord's other calculation methods might be of interest in that case.
I'm to sum 2 fields on the level a model:
def update_sum_times
update_attribute(:sum_of_fields, calc_two_dates)
end
def calc_two_dates
date_one + date_two
end

Resources