Failing validations in join model when using has_many :through - ruby-on-rails

My full code can be seen at https://github.com/andyw8/simpleform_examples
I have a join model ProductCategory with the following validations:
validates :product, presence: true
validates :category, presence: true
My Product model has the following associations:
has_many :product_categories
has_many :categories, through: :product_categories
When I try to create a new product with a category, the call to #product.save! in the controller fails with:
Validation failed: Product categories is invalid
When I remove the validations, everything works and the join models are saved correctly.
I'm using strong_parameters but I don't think that should be related to this issue.

This is a "racing condition" in the callback chain.
When you create a product it doesn't have any id before it is saved, therefore there is no product in the scope of ProductCategory.
Product.new(name: "modern times", category_ids:[1, 2]) #=> #<Product id: nil >
At that stage of validation (before saving), ProductCatgory cannot assign any id to it's foreign key product_id.
That's the reason you have association validations : so that the validation happens in the scope of the whole transaction
UPDATE: As said in the comment you still can't ensure presence of a product/category. There's many ways around depending on why you want do this (e.g direct access to ProductCategory through some form)
You can create a flag to have validates :product, presence: true, if: :direct_access?
or if you can only update them: validates :product, presence: true, on: "update"
create your product first (in the products_controller) and add the categories after
... But indeed these are all compromises or workarounds from the simple #product.create(params)

Specifying inverse_of on your joining models has been documented to fix this issue:
https://github.com/rails/rails/issues/6161#issuecomment-6330795
https://github.com/rails/rails/pull/7661#issuecomment-8614206
Simplified Example:
class Product < ActiveRecord::Base
has_many :product_categories, :inverse_of => :product
has_many :categories, through: :product_categories
end
class Category < ActiveRecord::Base
has_many :product_categories, inverse_of: :category
has_many :products, through: :product_categories
end
class ProductCategory < ActiveRecord::Base
belongs_to :product
belongs_to :category
validates :product, presence: true
validates :category, presence: true
end
Product.new(:categories => [Category.new]).valid? # complains that the ProductCategory is invalid without inverse_of specified
Adapted from: https://github.com/rails/rails/issues/8269#issuecomment-12032536

Pretty sure you just need to define your relationships better. I still might have missed some, but hopefully you get the idea.
class Product < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
validates :name, presence: true
validates :description, presence: true
validates :color_scheme, presence: true
belongs_to :color_scheme
has_many :product_categories, inverse_of: :product
has_many :categories, through: :product_categories
end
class ProductCategory < ActiveRecord::Base
belongs_to :product
belongs_to :category
validates_associated :product
validates_associated :category
# TODO work out why this causes ProductsController#create to fail
# validates :product, presence: true
# validates :category, presence: true
end
class Category < ActiveRecord::Base
has_many :product_categories, inverse_of: :category
has_many :products, through: :product_categories
end

Related

How to add has_many associations to record on create?

I have the following Rails models:
class Project < ApplicationRecord
validates :title, presence: true
validates :project_managers, length: { minimum: 1, message: "Please assign at least one PM" }
has_many :project_assignments, dependent: :destroy
has_many :project_managers, through: :project_assignments, source: :user
end
class User < ApplicationRecord
has_many :project_assignments, dependent: :destroy
has_many :projects, through: :project_assignments
end
class ProjectAssignment < ApplicationRecord
belongs_to :project
belongs_to :user
end
Now I'm trying to add project managers while creating the record:
Project.create!(title: "foobar", project_manager_ids: [1])
But this leads too ActiveRecord::RecordInvalid: Validation failed: Project assignments is invalid
Is there a way to add project managers directly on create?
In your project model, add the following
accepts_nested_attributes_for :project_assignments, allow_destroy: true
In your Project controller, in strong params add the following
def params
params.require(:project).permit(
:id,
:title,
project_assignments_attributes: [
:id,
:user_id,
_destroy
]
)
end

Trying to match Products and Requests through Categories

I'm making a rails marketplace app for uni where Users can be matched with specific products based on their request.
Users can list products that have specific categories.
Users can also list Requests where they can specify what products they're looking and their categories.
The aim is to match the request to a particular product based on the matching categories
Here are my models
class Product < ApplicationRecord
belongs_to :user
has_many_attached :images, dependent: :destroy
has_many :product_categories
has_many :categories, through: :product_categories
validates :user_id, presence: true
end
class Category < ApplicationRecord
has_many :product_categories
has_many :products, through: :product_categories
validates :name, presence: true, length: { minimum: 3, maximum: 25}
validates_uniqueness_of :name
end
class ProductCategory < ApplicationRecord
belongs_to :product
belongs_to :category
end
class Request < ApplicationRecord
belongs_to :user
has_many_attached :images, dependent: :destroy
has_many :request_categories
has_many :categories, through: :request_categories
validates :user_id, presence: true
end
class RequestCategory < ApplicationRecord
belongs_to :request
belongs_to :category
end
I was thinking of creating a new model called Match to bring together the product and categories or is it easier to match it in the request?
In my mind, your new Match class would essentially be a join table for a has_many :through association. Assuming that you're implementing an asynchronous worker (e.g. Sidekiq / ActiveJob) to go through and make "matches", you'll want to connect matches to a particular Request, and likely store some meta-data (has the user seen the Match yet? Have they rejected it?)
So, I'd probably generate a Match class like this:
rails g model Match seen_at:datetime deleted_at:datetime request:references product:references
And set up the associations as follows:
class Match < ApplicationRecord
belongs_to :request
belongs_to :product
end
class Request < ApplicationRecord
belongs_to :user
has_many_attached :images, dependent: :destroy
has_many :request_categories
has_many :categories, through: :request_categories
has_many :matches
has_many :products, through: :matches
validates :user_id, presence: true
end
class Product < ApplicationRecord
belongs_to :user
has_many_attached :images, dependent: :destroy
has_many :product_categories
has_many :categories, through: :product_categories
has_many :matches
has_many :requests, through: :matches
validates :user_id, presence: true
end
Also, you'll likely want to add the Request has_many :through to your Category model (I think you forgot that one):
class Category < ApplicationRecord
has_many :product_categories
has_many :products, through: :product_categories
has_many :request_categories
has_many :requests, through: :request_categories
validates :name, presence: true, length: { minimum: 3, maximum: 25}
validates_uniqueness_of :name
end
The big part of the job is working out how to have your app periodically look for matches - you may want to start with the Active Job Basics documentation.

AcriveRecord relationship validation between two or more belongs_to associations

I'm wondering if there is a cleaner way to validate multiple relationships in rails. I don't mean validating an association, rather ensuring relationship integrity between two or more belongs_to associations. Here is some code as an example:
class User < ActiveRecord::Base
has_many :products, inverse_of: :user
has_many :purchases, inverse_of: :user
end
class Purchase < ActiveRecord::Base
belongs_to :user, inverse_of: :purchases
has_many :products, inverse_of: :purchase
end
class Product < ActiveRecord::Base
belongs_to :user, inverse_of: :products
belongs_to :purchase, inverse_of: :products
validates :user, :purchase, presence: true
validate :purchase_user
private
def purchase_user
errors.add(:purchase, 'purchase user does not match user') if user != purchase.user
end
end
The purchase_user validation method here checks that the user is the same as purchase.user and adds an error if they are not.
I could change it to use :inclusion like so:
validates :purchase, inclusion: { in: proc { |record| record.user.purchases } }
But that seems even more inefficient, any suggestions?

Accepted_nested_attributes_for relation for has_many in Rails 4

I've got the following order_params:
{"user_id":"1","order_status_id":"1","delivery_type_id":"1","delivery_time":"10","order_items":[{"count":"5","item_id":"1"}]}
These params are used for creating an Order object with nested order_item objects. Models:
class Order < ActiveRecord::Base
validates :user, :order_status, :delivery_type, presence: true
belongs_to :user
belongs_to :order_status
belongs_to :delivery_type
has_many :order_items
accepts_nested_attributes_for :order_items
end
Order_item:
class OrderItem < ActiveRecord::Base
validates :item, :order, :count, presence: true
belongs_to :item
belongs_to :order
end
But when I'm trying to create a new Order:
Order.create!(order_params)
I got the following error:
OrderItem(#70182455585540) expected, got ActiveSupport::HashWithIndifferentAccess(#70182454696760)
How can I fix it? Thanks in advance.
You key for order_items attributes is wrong, which should be order_items_attributes if you use nested attributes.
...,"order_items":[{"count":"5","item_id":"1"}]}
should be
...,"order_items_attributes":[{"count":"5","item_id":"1"}]}

Rails has_many :through validation

I'm having trouble validating a model from a has_many through association. Below are the relevant models:
Broadcast Model
class Broadcast < ActiveRecord::Base
attr_accessible :content,
:expires,
:user_ids,
:user_id
has_many :users, through: :broadcast_receipts
has_many :broadcast_receipts, dependent: :destroy
validates :user_id, presence: true
validates :content, presence: true
end
Broadcast Receipt Model
class BroadcastReceipt < ActiveRecord::Base
belongs_to :broadcast
belongs_to :user
attr_accessible :user_id, :cleared, :broadcast_id
validates :user_id , presence: true
validates :broadcast_id , presence: true
end
There is also an association with Users that have_many broadcasts receipts through broadcast receipts.
The problem appears to be with the following line:
validates :broadcast_id , presence: true
Whenever I try to create a Broadcast, I get a rollback with no error messages given. However, when removing the above line, everything works as expected.
This looks like a problem with the Broadcast not being saved before the Broadcast Receipts are being created.
Is there any way I'd be able to validate the broadcast_id is set on the receipt model?
This appears to be the same issue discussed here: https://github.com/rails/rails/issues/8828, which was solved by adding :inverse of to the has_many associations to the join model.
There might be some problem in your code structuring. You could give this version a try.
class Broadcast < ActiveRecord::Base
# I assume these are the recipients
has_many :broadcast_receipts, dependent: :destroy
has_many :users, through: :broadcast_receipts
# I assume this is the creator
validates :user_id, :content, presence: true
attr_accessible :content, :expires, :user_id, :user_ids
end
class BroadcastReceipt < ActiveRecord::Base
belongs_to :broadcast
belongs_to :user
# You should be able to validate the presence
# of an associated model directly
validates :user, :broadcast, presence: true
attr_accessible :cleared
end

Resources