How to bulk validate association in Rails - ruby-on-rails

I have the following scenario:
One of my models, let's call it 'Post', has multiple associated models, Images.
One, and only one, of those images can be the key Image to its Post (that is represented as a boolean flag on the Image model and enforced through a validation on the Image model which uses the Post as its scope).
Now of course when I want to update the primary Image flag, it happens that an Image model's key flag is set to true and the validation fails because there's still another Image with the key flag set to true.
I know, that thing screams to be transformed into an association on the Post model, which links to the key Image, but is there a way to validate associations in bulk in Rails?
What would be your take, would you make the key Image a separate association on the Post model or would you use the boolean flag?

there is a simple solution but it needs some trust:
Remove the validation "is there only one primary image?"
Make sure there will be only one primary image by adding a filter
The big plus is that you don't have to check anything in your controller or post model. Just take an image, set is_primary to true and save it.
So the setup could look like:
class Post < ActiveRecord::Base
has_many :images
# some sugar, #mypost.primary_image gets the primary image
has_one :primary_image,
:class_name => "Image",
:conditions => {:is_primary => true }
end
class Image < ActiveRecord::Base
belongs_to :post
# Image.primary scopes on primary images only
scope :primary, where(:is_primary => true)
# we need to clear the old primary if:
# this is a new record and should be primary image
# this is an existing record and is_primary has been changed to true
before_save :clear_primary,
:if => Proc.new{|r| (r.new_record? && r.is_primary) || (r.is_primary_changed? && r.is_primary) }
def clear_primary
# remove old primary image
Image.update_all({:is_primary => false}, :post_id => self.post_id)
end
end
Edit:
This will work in any case - why?
before_save is only invoked if all validations succeed
the whole save is wrapped in a transaction, this means if clear_primary or the saving of the image itself fails, everyhing will be rolled back to it's original state.

Well you can do this within your Post model:
# Post.rb
has_many :images, :conditions => ['primary = ?', false]
has_one :primary_image, :conditions => ['primary = ?', true]
When you want to change the primary image, do something like this:
# Post.rb
def new_primary_image(image_id)
primary_image.primary = false
Image.find(image_id).primary = true
end

Related

Product Model With Stock_QTY Scoped to Size

I have a functioning, self built e-com web app, but right now the app assumes we have infinite quantity.
It uses line_items and product models.
I am going to add stock_QTY as an attribute to the product
For items that don't have any variants (sizes, colors etc.), the line_item will be created if and stock_QTY is greater than one.
I'm not sure how to deal with sizes though.
Should I create different Products? IE:
Shirt.create (name:"small green shirt", color:"green", size:S, stock_QTY:4)
Shirt.create (name:"medium green shirt", color:"green", size:M, stock_QTY:6)
Shirt.create (name:"large green shirt", color: "green", size:L, stock_QTY:1)
This seems repetitive, but at least the stock QTY can have some independence. Is there a way to create only one shirt record, with variants, and allow them to have different sizes?
Ideally I'd like
Shirt.create(name:"shirt", colors:['red', 'blue', 'green'], sizes: ['s','m',l'])
and then be able to do
Shirt.where(color => "green").where(size => "L").stock_QTY
=> X number
Shirt.where(color => "green").where(size => "M").stock_QTY
=> Y number
This way I have one model, but it can store different quantities depending on the scope of the variants.
Let me know if this is unclear.
Thanks!
Update
Product.rb
require 'file_size_validator'
class Product < ActiveRecord::Base
has_many :line_items
before_destroy :ensure_not_referenced_by_any_line_item
mount_uploader :image, ImageUploader
validates :price, :numericality => {:greater_than_or_equal_to => 0.01}
validates :title, :uniqueness => true
def to_param
"#{id}_#{permalink}"
end
private
# ensure that there are no line items referencing this product
def ensure_not_referenced_by_any_line_item
if line_items.empty?
return true
else
errors.add(:base, 'Line Items present')
return false
end
end
end
Here is my Product as it is now.
from seeds.rb
Product.create!([
{
:title => "Liaisons Shirt Green",
:description => "",
:has_size => true,
:price => 24.99,
:permalink => "shirt",
:weight => 16.00,
:units => 1.00,
:image => (File.open(File.join(Rails.root, "app/assets/images/dev7logo.png")))
}
])
So, my advice is to improve the DB schema to make it more flexible and scalable ;)
Define the Size and Color models (2 new tables), make your actual Product model the BaseProduct model (just renaming the table) and finally create the Product model (new table) which will have 3 external keys (base_product_id, color_id and size_id) and of course the stock_qty field to define all possible configurations with the minimal repetition of information :)!
Just a little help, you're final classes schema should be like:
class Color < ActiveRecord::Base
end
class Size < ActiveRecord::Base
end
class BaseProduct < ActiveRecord::Base
# This will have almost all fields from your actual Product class
end
class Product < ActiveRecord::Base
# Ternary table between Color, Size and BaseProduct
end
I'm omitting all associations because I like the idea you succeed on your own, but if you need, please just ask :)
This will allows you to do BaseProduct queries like:
base_product.colors
base_product.sizes
product.base_product # retrieve the base_product from a product
and to keep trace of the quantities:
product.stock_qty
product.color
product.size # size and color are unique for a specific product
You can also create some helper method to make the creation process similar to the one you'd like to have (as shown in your question).
Well I understand the approaches you wanted to deal with. Pretty easy business logic if I understand correctly. So you wanted the following things If I get you correctly:
You have so many products
You want to add stock count record
You wanted to validate the product for selling (line items for cart) if the product available
You need to ensure if the product is already in customer's cart when you are deleting that.
So I assumed you already added the stock_qty columns.
Now you need to ensure if the product is available to be added in your cart.
So you need to write your validation in your line_item modem.
class LineItem < ActiveRecord::Base
# other business logics are here
belongs_to :product
before_validation :check_if_product_available
def check_if_product_available
# you will find your product from controller, model should be responsible to perform business
# decision on them.
if !self.try(:product).nil? && self.product.stock_qty < 1
errors.add(:product, 'This product is not available in the stock')
return false
end
end
end
This is the approach I believe is the valid way to do. And moreover, rather saving variants in same product model, I would suggest consider designing your model more efficiently with separate variant model or you can utilize the power of self association.
I hope this will help you. Let me know if I miss anything or miss interpret your problem.
Thanks

Rails Active Record Nested Attributes Validation which are in the same request

I have two models house and booking.Everything is okey over booking_date validation. But when I try to update or create multi booking in the same request. Validation can't check the invalid booking in the same request params.
Let give an example assume that booking table is empty.
params = { :house => {
:title => 'joe', :booking_attributes => [
{ :start_date => '2012-01-01', :finish_date => '2012-01-30 },
{ :start_date => '2012-01-15', :finish_date => '2012-02-15 }
]
}}
Second booking also save but its start_date is between first booking interval. When I save them one by one validation works.
class House < ActiveRecord::Base
attr_accessible :title, :booking_attributes
has_many :booking
accepts_nested_attributes_for :booking, reject_if: :all_blank, allow_destroy: true
end
class Booking < ActiveRecord::Base
belongs_to :house
attr_accessible :start_date, :finish_date
validate :booking_date
def booking_date
# Validate start_date
if Booking.where('start_date <= ? AND finish_date >= ? AND house_id = ?',
self.start_date, self.start_date, self.house_id).exists?
errors.add(:start_date, 'There is an other booking for this interval')
end
# Validate finish_date
if Booking.where('start_date <= ? AND finish_date >= ? AND house_id = ?',
self.finish_date, self.finish_date, self.house_id).exists?
errors.add(:finish_date, 'There is an other booking for this interval')
end
end
end
I google nearly 2 hours and could not find anything. What is the best approach to solve this problem?
Some resources
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
http://railscasts.com/episodes/196-nested-model-form-part-1
This was only a quick 15-minutes research on my part, so I may be wrong, but I believe here's the root cause of your problem:
What accepts_nested_attributes_for does under the hood, it calls 'build' for new Booking objects (nothing is validated at this point, objects are created in memory, not stored to db) and registers validation and save hooks to be called when the parent object (House) is saved. So, in my understanding, all validations are first called for all created objects (by calling 'valid?' for each of them. Then, again if I get it right, they are saved using insert_record(record,false) which leads to save(:validate => false), so validations are not called for the 2nd time.
You can look at the sources inside these pages: http://apidock.com/rails/v3.2.8/ActiveRecord/AutosaveAssociation/save_collection_association,
http://apidock.com/rails/ActiveRecord/Associations/HasAndBelongsToManyAssociation/insert_record
You validations call Booking.where(...) to find the overlapping dates-ranges. At this point the newly created Booking objects are still only in memory, not saved to the db (remember, we are just calling valid? for each of them in the loop, saves will be done later). Thus Booking.where(...) which runs a query against a DB doesn't find them there and returns nothing. Thus they all pass valid? stage and then saved.
In a nutshell, the records created together in such a way will not be cross-validated against each other (only against the previously existing records in the database). Hence the problem you see.
Thus either save them one-by-one, or check for such date-overlapping cases among the simultaneously created Bookings yourself before saving.

Rails counter_cache not updating correctly

Using Rails 3.1.3 and I'm trying to figure out why our counter caches aren't being updated correctly when changing the parent record id via update_attributes.
class ExhibitorRegistration < ActiveRecord::Base
belongs_to :event, :counter_cache => true
end
class Event < ActiveRecord::Base
has_many :exhibitor_registrations, :dependent => :destroy
end
describe ExhibitorRegistration do
it 'correctly maintains the counter cache on events' do
event = Factory(:event)
other_event = Factory(:event)
registration = Factory(:exhibitor_registration, :event => event)
event.reload
event.exhibitor_registrations_count.should == 1
registration.update_attributes(:event_id => other_event.id)
event.reload
event.exhibitor_registrations_count.should == 0
other_event.reload
other_event.exhibitor_registrations_count.should == 1
end
end
This spec fails indicating that the counter cache on event is not being decremented.
1) ExhibitorRegistration correctly maintains the counter cache on events
Failure/Error: event.exhibitor_registrations_count.should == 0
expected: 0
got: 1 (using ==)
Should I even expect this to work or do I need to manually track the changes and update the counter myself?
From the fine manual:
:counter_cache
Caches the number of belonging objects on the associate class through the use of increment_counter and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it’s destroyed.
There's no mention of updating the cache when an object is moved from one owner to another. Of course, the Rails documentation is often incomplete so we'll have to look at the source for confirmation. When you say :counter_cache => true, you trigger a call to the private add_counter_cache_callbacks method and add_counter_cache_callbacks does this:
Adds an after_create callback which calls increment_counter.
Adds an before_destroy callback which calls decrement_counter.
Calls attr_readonly to make the counter column readonly.
I don't think you're expecting too much, you're just expecting ActiveRecord to be more complete than it is.
All is not lost though, you can fill in the missing pieces yourself without too much effort. If you want to allow reparenting and have your counters updated, you can add a before_save callback to your ExhibitorRegistration that adjusts the counters itself, something like this (untested demo code):
class ExhibitorRegistration < ActiveRecord::Base
belongs_to :event, :counter_cache => true
before_save :fix_counter_cache, :if => ->(er) { !er.new_record? && er.event_id_changed? }
private
def fix_counter_cache
Event.decrement_counter(:exhibitor_registration_count, self.event_id_was)
Event.increment_counter(:exhibitor_registration_count, self.event_id)
end
end
If you were adventurous, you could patch something like that into ActiveRecord::Associations::Builder#add_counter_cache_callbacks and submit a patch. The behavior you're expecting is reasonable and I think it would make sense for ActiveRecord to support it.
If your counter has been corrupted or you've modified it directly by SQL, you can fix it.
Using:
ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters)
Example 1: Re-compute the cached count on the post with id = 17.
Post.reset_counters(17, :comments)
Source
Example 2: Re-compute the cached count on all your articles.
Article.ids.each { |id| Article.reset_counters(id, :comments) }
I recently came across this same problem (Rails 3.2.3). Looks like it has yet to be fixed, so I had to go ahead and make a fix. Below is how I amended ActiveRecord::Base and utilize after_update callback to keep my counter_caches in sync.
Extend ActiveRecord::Base
Create a new file lib/fix_counters_update.rb with the following:
module FixUpdateCounters
def fix_updated_counters
self.changes.each {|key, value|
# key should match /master_files_id/ or /bibls_id/
# value should be an array ['old value', 'new value']
if key =~ /_id/
changed_class = key.sub(/_id/, '')
changed_class.camelcase.constantize.decrement_counter(:"#{self.class.name.underscore.pluralize}_count", value[0]) unless value[0] == nil
changed_class.camelcase.constantize.increment_counter(:"#{self.class.name.underscore.pluralize}_count", value[1]) unless value[1] == nil
end
}
end
end
ActiveRecord::Base.send(:include, FixUpdateCounters)
The above code uses the ActiveModel::Dirty method changes which returns a hash containing the attribute changed and an array of both the old value and new value. By testing the attribute to see if it is a relationship (i.e. ends with /_id/), you can conditionally determine whether decrement_counter and/or increment_counter need be run. It is essnetial to test for the presence of nil in the array, otherwise errors will result.
Add to Initializers
Create a new file config/initializers/active_record_extensions.rb with the following:
require 'fix_update_counters'
Add to models
For each model you want the counter caches updated add the callback:
class Comment < ActiveRecord::Base
after_update :fix_updated_counters
....
end
A fix for this has been merged in to active record master
https://github.com/rails/rails/issues/9722
The counter_cache function is designed to work through the association name, not the underlying id column. In your test, instead of:
registration.update_attributes(:event_id => other_event.id)
try
registration.update_attributes(:event => other_event)
More information can be found here: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

Rails - How to prevent deleting all children records of a parent

Listing < AR
has_many :images
accepts_nested_attributes_for :images, :allow_destroy => true
validate :validate_image_count
def validate_image_count
errors.add_to_base("too few") if images.length < 1
end
end
Image < AR
belongs_to :listing
end
In my Listing#edit form I use fields_for to provide fields for all the images along with checkboxes to delete images. This is working fine. I want to enforce a check such that a listing is valid only if it had at least one image and at most 6.
In my current setup I can go to edit and delete all the images, and then update the listing.
I have tried using a validation as shown above but thats not being called. Could be just the way nested_attributes work in rails. Whats the best way to enforce this check?
as the images won't be deleted when you call the validation method it would return true on the image length. You can use marked_for_destruction?
def validate_image_count
errors.add_to_base("too few") self.images.any? { |i| i.marked_for_destruction? }
end

Calling valid? on new records, but ignore validity of built associations

I have three tables: tasks, departments, and department_tasks. I need to call "valid?" on new task objects, but I want to ignore the validity of any "built" department_tasks. We are doing bulk uploads, and so we load everything or nothing.
As we loop through the Excel file we are reading in, we build the new "Task" according to the values in each row. With each row, there may be an associated department for the task; if there is, we "build" the associated department_task object like so:
new_task.department_tasks.build(:department_id => d.id)
At the end of the loop, we test the validity of the new "task" object by calling "valid?"
new_task.valid?
If the task is valid, it goes in the "good" pile; if it's bad, it goes on the "bad" pile.
The problem is, we haven't saved the task and therefore it has no :id. Without an id, the "built" department_task is invalid (:department_id and :task_id must both be present).
I need to know how I can call "valid?" or test validity of the "new_task" object without the validation cascading down to the "task_department" associated object which cannot be valid before task is saved.
You can skip individual validations using :if or :unless
validates_presence_of :department_id,
:unless => lambda { |record| record.new_record? }
If I understand correctly, you have something like this:
class Task < ActiveRecord::Base
has_many :department_tasks
has_many :departments, :through => :department_tasks
validates_associated :department_tasks
end
class DepartmentTask < ActiveRecord::Base
belongs_to :task
belongs_to :department
validates_presence_of :department_id, :task_id
end
When Task is new the associated validation in DepartmentTask fails because task_id is nil. Correct?
I don't see an easy way around this. The most obvious solution is to just remove the validates_presence_of for task_id. If the only way that you create DepartmentTasks is to build them through the Task model, the presence_of validation seems unnecessary, since Rails will always add the task_id when Task is saved.
Another option is to wrap it in a transaction, create the new task (so it has an ID), then build and validate DepartmentTask, and rollback if invalid.
You should assign an object to your AR queries to check it's validity, also use .new(:department_id => d.id)
myrecord = new_task.department_tasks.new(:department_id => d.id)
if myrecord.save!
"good pile"
else
"bad pile"
end

Resources