Rails Has Many Validate Unique Attribute With Accepts Nested Attributes For - ruby-on-rails

I have Invoices with many Invoice Line Items. Invoice line items point to a specific item. When creating or updating an Invoice, I'd like to validate that there is not more than 1 invoice line item with the same Item (Item ID). I am using accepts nested attributes and nested forms.
I know about validates_uniqueness_of item_id: {scope: invoice_id}
However, I cannot for the life of me get it to work properly. Here is my code:
Invoice Line Item
belongs_to :item
validates_uniqueness_of :item_id, scope: :invoice_id
Invoice
has_many :invoice_line_items, dependent: :destroy
accepts_nested_attributes_for :invoice_line_items, allow_destroy: true
Invoice Controller
// strong params
params.require(:invoice).permit(
:id,
:description,
:company_id,
invoice_line_items_attributes: [
:id,
:invoice_id,
:item_id,
:quantity,
:_destroy
]
)
// ...
// create action
def create
#invoice = Invoice.new(invoice_params)
respond_to do |format|
if #invoice.save
format.html { redirect_to #invoice }
else
format.html { render action: 'new' }
end
end
end
The controller code is pretty standard (what rails scaffold creates).
UPDATE - NOTE that after more diagnosing, I find that on create it always lets me create multiple line items with the same item when first creating an invoice and when editing an invoice without modifying the line items, but NOT when editing an invoice and trying to add another line item with the same item or modifying an attribute of one of the line items. It seems to be something I'm not understanding with how rails handles nested validations.
UPDATE 2 If I add validates_associated :invoice_line_items, it only resolves the problem when editing an already created invoice without modifying attributes. It seems to force validation check regardless of what was modified. It presents an issues when using _destroy, however.
UPDATE 3 Added controller code.
Question - how can I validate an attribute on a models has many records using nested form and accepts nested attributes?

I know this isn't directly answering your qestion, but I would do things a bit differently.
The only reason InvoiceLineItem exists is to associate one Invoice to many Items.
Instead of having a bunch of database records for InvoiceLineItem, I would consider a field (e.g. HSTORE or JSONB) that stores the Items directly to the Invoice:
> #invoice.item_hash
> { #item1: #quantity1, #item2: #quantity2, #item3: #quantity3, ... }
Using the :item_id as a key in the hash will prevent duplicate values by default.
A simple implementation is to use ActiveRecord::Store which involves using a text field and letting Rails handle serialization of the data.
Rails also supports JSON and JSONB and Hstore data types in Postgresql and JSON in MySQL 5.7+
Lookups will be faster as you don't need to traverse through InvoiceLineItem to get between Invoice and Item. And there are lots of great resources about interacting with JSONB columns.
# invoice.rb
...
def items
Item.find( item_hash.keys)
end
It's a bit less intuitive to get "invoices that reference this item", but still very possible (and fast):
# item.rb
...
# using a Postgres JSON query operator:
# jsonb ? text → boolean (Does the text string exist as a top-level key or array element within the JSON value?)
# https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE
def invoices
Invoice.where('item_hash ? :key', key: id)
end

After reading this through a few times I think see it as:
An invoice has many invoice_line_items
An invoice has many items through invoice_line_items
Item_id needs to be unique to every invoice, thus no repeating of items in the line_item list.
Items are a catalogue of things that can show up in multiple invoices. i.e. items are things like widget_one and widget_two, and an invoice can only contain one line with widget one, but many invoices could contain this same item. If this is not true and an item will only ever show up in one invoice, let me know and I will change my code.
So I think your validation should not be in Items, as items know nothing about invoices. You want to make sure your join table has no entries where a given invoice_id has duplicate item_id entries.
item.rb:
has_many :invoice_line_items
has_many :invoices, through: :invoice_line_items
invoice.rb:
has_many :invoice_line_items
has_many :items, through: :invoice_line_items
invoice_line_item.rb:
belongs_to :item
belongs_to :invoice
validates_uniqueness_of :item_id, :scope => :invoice_id
This SHOULD give you what you need. When you create a new invoice and save it, Rails should try to save each of the line items and the join table. When it hits the duplicate item_id it should fail and throw an error.
If you are going to have unique constraints in your code I would alway back it up with a constraint in your database so that if you do something that makes an end run around this code it will still fail. So a migration should be added to do something like:
add_uniq_constraint_to_invoice_line_items.rb:
def change
add_index :invoice_line_items, [:invoice_id, :item_id], unique: true
end
This index will prevent creation of a record with those two columns the same.

Related

Rails accepts_nested_attributes_for with belongs_to. Why I can't set id?

I use Rails 5.1.6 and have troubles with accepts_nested_attributes_for.
I have two models
class Material < ApplicationRecord
belongs_to :rubric, optional: true
accepts_nested_attributes_for :rubric
end
class Rubric < ApplicationRecord
has_many :materials, dependent: :nullify
end
I try to set rubric id to new item by rubric_attributes.
describe 'create material' do
it 'should set rubric: :id' do
# prepare
item = FactoryBot.build(:material)
rubric = FactoryBot.create(:rubric)
# action
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
# check
expect(item.valid?).to eq(true)
expect(item.save).to eq(true)
expect(item.rubric_id).to eq(rubric.id)
end
end
But I have an error:
Failure/Error:
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
ActiveRecord::RecordNotFound:
Couldn't find Rubric with ID=1 for Material with ID=1
And I have the same error with updating a material.
Is it a predictable behavior of accepts_nested_attributes_for, and I can't use rubric_attributes for setting existed rubric id?
Docs say:
For each hash that does not have an id key a new record will be instantiated, unless the hash also contains a _destroy key that evaluates to true.
It suggest that if you pass id in nested attributes, it's treated as an existing record that should be updated.
You most likely don't need accepts_nested_attributes_for in the first place.
If you want the user to be able to select records with a select you don't actually need to do anything besides create a select and whitelist the material_id attribute:
<%= form_for(#material) do |f| %>
<div class="field">
<%= f.label :rubic_id %>
<%= f.collection_select :rubic_id, Rubic.all :id, :name %>
</div>
<%= f.submit %>
<% end %>
The select will create an array in the params.
class MaterialsController
# POST /materials
def create
#material = Material.new(material_params)
if #material.save
redirect_to #material
else
render :new
end
end
private
def material_params
params.require(:material)
.permit(:foo, :bar, material_ids: [])
end
end
accepts_nested_attributes_for is really intended for the case where you need to create / edit nested resources in the same request. The only reason you would use it here is:
The user should be able to create the material in the same form.
You have a join table with additional attributes (like quantity for example) which you want the user to be able to set.
You can still do 1. together with the select above, but you can't use accepts_nested_attributes_for to set a simple belongs_to association. Nor would you want to either as its like using a rocket to beat in a nail.
Just leaving this in case somebody else may have a problem as I did, populating nested children records in a Rails backend via an API, but using hash_ids via friendly_id.
Came about this when trying to PATCH Rails records via an API. First setup was to mirror the Rails way of sending the record values in nested form fashion. Meaning, I've purposefully built the params hash I was sending from the frontend over to a Rails backend like in a typical nested form transmission:
{ "children": {
"0": {
"name": "foo",
"category_id": "1",
"hash_id": "HDJPQT"
}
}
accepts_nested_attributes_for needs id to PATCH records. Otherwise it is going to create a new record altogether. Which i did not want in my scenario. I was sending over hash_id and therefore creating new records unintentionally.
Solution
For the time being I am not replicating a nested form value hash anymore to send to the Rails backend anymore. Instead I am simply updating children records separately in a chained fetch query from the Javascript frontend.
Note:
If you want to keep sending a mirrored nested form array of hashes, there could be a way to change the primary key of the database table to hash_id or UUID, depending on your needs. Have not yet tested this solution.

Rails - Validate nested attribute on two fields

I have two models Customer and Item, Customer has_many :items and Item belongs_to :customer and Customer accepts_nested_attributes_for :items. An item can be returned or not returned, but what I need to do, is validate on create that the item.number is not repeated for the not_returned items. My validation is as follows:
def unique_number
if Item.not_returned.find_all_by_number(self.number).to_a.size > 0
errors.add(:number, "duplicate number, please use another")
end
end
def self.not_returned
where("returned = false")
end
But it doesn't work if I add two newly created items with the same number, not sure why, but I need to validate this even when two records are being created at the same time, any ideas?
Thanks in advance

Rails Has Many Association

I have few question that bugs me off and need to be answered. Everything is related to the following tutorial Two Many-to-Many
Question 1
Does the join table using has_many need to have an id? or its best practice to remove the id? and add an index and using the two other primary key and set it unique and together?
Question 2
How can it be done in the migration of creating a table?
Question 3
After doing these relationship model and updating the data. I would like to create a new set of data everytime it is updated (to preserve the data). How would a controller would look in the update, new, create model?
Question 4
In the the middle table, I would like to set attributes such has a visible true, or false, how can I set also not just the third table but also the second table arguments
First ... a word of caution: That railscast is very old. There may be syntactical things in that episode that have been dated by new versions of rails.
Question 1
If you are using the has_many through method then you have to have an id column in the join model because you are using a full blown model. As Ryan mentions in the episode, you'll choose this method if you need to track additional information. If you use the has_and_belongs_to_many method, you will not have an id column in your table.
If you want to achieve a check where you do not allow duplicates in your many-to-many association (ie allow the pairing of item a with item b and again allowing another record of item a to item b), you can use a simple validates line with a scope:
validates_uniqueness_of :model_a_id, :scope => [:model_b_id]
Question 2
You can add indices in your migrations with this code
add_index :table_name, [ :join_a_id, :join_b_id ], :unique => true, :name => 'by_a_and_b'
This would be inserted into the change block below your create_table statement (but not in that create_table block). Check out this question for some more details: In a join table, what's the best workaround for Rails' absence of a composite key?
Question 3
I'm not completely clear on what you're looking to accomplish but if you want to take some action every time a new record is inserted into the join model I would use the after_create active record hook. That would look something like this.
class YourJoinModel < ActiveRecord::Base
after_create :do_something
def do_something
puts "hello world"
end
end
That function, do_something, will be called each time a new record is created.
Question 4
Using the has_many through method will give you access to the additional attributes that you defined in that model on both sides of the relationship. For example, if you have this setup:
class Factory < ActiveRecord::Base
has_many :widgets, :through => :showcases
end
class Widget < ActiveRecord::Base
has_many :factories, :through => :showcases
end
class Showcases < ActiveRecord::Base
belongs_to :factory
belongs_to :widget
attr_accessiable :factory_id, :widget_id, :visible
end
You could say something like
widget = Widget.first
shown = widget.showcases
shown.first.visible
or
shown = widget.showcases.where( :visible=> true )
You can also reach to the other association:
shown.first.factory
The reason for having an id column in an association is it gives you a way of deleting that specific association without concerning yourself with the relationship it has. Without that identifier, associations are hard to define outside of specifying all foreign keys.
For a trivial case where you have only two components to your key, this isn't that big a differentiator, but often you will have three or more as part of your unique constraint and there's where things get tricky.
Having an id also makes the relationship a first-class model. This can be useful when you're manipulating elements that have associated meta-data. It also means you can add meta-data effortlessly at a later date. This is what you mean by your "Question 4". Add those attributes to the join model.
Generally the join model is created like you would any other model. The primary key is the id and you create a series of secondary keys:
create_table :example_things |t|
t.integer :example_id
t.integer :thing_id
end
add_index :example_joins, [ :example_id, :thing_id ], :unique => true
add_index :example_joins, :thing_id
The main unique index serves to prevent duplication and allows lookups of key-pairs. The secondary serves as a way of extracting all example_id for a given thing_id.
The usual way to manipulate meta-data on the join model is to fetch those directly:
#example_things = #example.example_things.includes(:thing)
This loads both the ExampleThing and Thing models associated with an Example.

Rails complex nested forms with 3 models

This question concerns three models:
Sale
class Sale < ActiveRecord::Base
has_many :sale_items
has_many :items, through :sale_items
end
Item
class Item < ActiveRecord::Base
has_many :sale_items
has_many :sales, :through => :sale_items
end
SaleItem
class SaleItem < ActiveRecord::Base
belongs_to :sale
belongs_to :item
end
To explain, an item acts as a base template for a sale_item. The application has many Items, but these are not necessarily a part of every Sale. So, sale_item.name actually points to sale_item.item.name, and sale_item's price method looks like this:
def price
super || item.price
end
A sale_item either gets its price from its item, or that price can be overridden for that specific sale_item by editing its price column in the database.
This is what I'm having difficulty with in my sales/_form.html.erb view: I essentially need a table of all Item objects that looks the table in this Tinkerbin: http://tinkerbin.com/46T7JAKs.
So, what that means is that if an unchecked checkbox gets checked and the form is submitted, a new SaleItem needs to be created with an item_id equal to that if the Item from the list, and with appropriate price and quantity fields (quantity is specific to SaleItem and does not exist for Item objects).
Additionally, if the Sale that is being edited already includes a specific SaleItem, that checkbox should already be checked when the form view is rendered (so unchecking a box for a row would delete the SaleItem object associated with that Item and this Sale).
I'm not sure how this could be done—maybe I'm doing it wrong from the beginning. I toyed with the idea of doing away with the SaleItem model altogether and just creating a items_sales table with the fields sale_id, item_id, price, and quantity, but I'm not sure that is the best pattern.
Update
The previous solution I posted ended up with some flaws and failing tests. I finally figured it out, but will post the real solution shortly.
As the author of cocoon, a gem that will make creating dynamically nested forms easier, I would like to point you to a sample project called cocoon_simple_form_demo, that contains all kinds of nested forms, including this type.
I have also written a blogpost describing all these.
Hope this helps.
What you want to have is a checkbox tag with an array which will hold on submission an array of all selected id's, so instead of the html checkbox use the checkbox helper:
<%= check_box_tag 'sale_item_ids[]', item.id -%>
On submission the params hash will hold the ids of the selected item's. What you need to do now is loop on each of these and do the appropriate creation of the relationship (sale_item). You must also loop on those that exist already for this sale and delete them if they are not in the array submited.
Upon creating the actual html page you can check if the id of the checkbox is already in the sale and check/uncheck it accordingly.
Hope this helps :)
What if you treat the join table more like a normal rails model.
class SaleItems < ActiveRecord::Migration
def change
create_table :sale_items do |t|
t.integer :sale_id
t.integer :item_id
t.float :price
t.float :quantity
t.timestamps
end
end
end
Then you can do things like
controller
#sale = Sale.new
#saleitem = #sale.SaleItem.build
You can add a item select for every line in the form. With some javascript you can update the price field from the item selected and change it if you want.
Even better than select, check out this railscast on auto complete association (if you don't have a pro account on railscast(get one!) go here
For adding lines til the Sale form take a look at this railscast on complex forms

Enforcing a Uniqueness Constraint in a Nested Form

I'm trying not to fight the defaults here and use Rails built-in support for nested attributes (from http://ryandaigle.com/articles/2009/2/1/what-s-new-in-edge-rails-nested-attributes). I'm labeling Things with Tags, and all works swell, and for each Thing I have a form with a nested field that creates a new Tag by a name. Trouble is, I need to make sure that each Tag has a unique name. Instead of creating a new Tag, if a user enters the name of one that already exists, I need to create the associate with that pre-existing Tag. How do I do this?
There's probably a better way to do this but this is about the best I can come up with for now.
In a has_many(:through) association accepts_nested_arguments_for uses an assignment to the virtual attribute #{association}_attributes to work its magic. It expects an array of hashes, where each hash contains attribute keys and their values. Any hashes with an id will be updated (or deleted if there is a :_delete key with the value of true). Any hashes missing an id will be used to create new items of that association. So the key is to intercept the call to tags_associations= and check any of the hashes that are missing ids for an existing tag with the same name value, and replace it with an entry that tags_attributes will use to make the association to the existing tag. N.B. for has_one and belongs_to relationships tag_attributes will expect a single hash. The code will be similar, but much simpler.
class Thing < ActiveRecord::Base
has_many :tags, :through => :taggings
has_many :taggings
accepts_nested_attributes_for :tags
def tags_attributes_with_recycling=(attributes)
existing_attributes = attributes.reject{|attribute| attribute[:id].nil?}
new_attributes = attributes - existing_attributes
new_and_recycled_attributes = new_attributes.map { |attribute|
tag_id = Tag.find_by_name(attribute[:name]).id
tag_id ? {:id => tag_id) : attribute
}
tags_attributes_without_recycling= (existing_attributes + new_and_recycled_attributes)
end
alias_method_chain :tags_attributes=, :recycling
end
It's untested, so no guarantees. But it should at least put you on track for a solution.
In your Thing class, define a tags_attributes= method to override the default and use Tag.find_or_create
def tags_attributes=(attributes)
attributes.each do |attr|
Tag.find_or_create_by_name(attr[:name])
end
end
Not sure what the attributes hash will look like but you get the idea.

Resources