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
Related
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.
I'm running Rails 5.2. I have a Cart and Item model and I want to combine the quantities and totals of duplicate items that are added to the cart.
My Cart accepts nested attributes for Item and I originally thought about using the reject_if condition to prevent the save of the "duplicate" item. However, I need to actually do this from the model since I have other scripts that can create a cart and items without submitting form data to the controller. From a callback in my Item model, how can I reject the save like I would with reject_if?
My original idea which I have abandoned:
class Cart < ApplicationRecord
has_many :items
accepts_nested_attributes_for :items, reject_if: proc { |attributes| attributes.function_that_decides_to_reject_or_not }
end
What I would like to achieve:
class Item < ApplicationRecord
belongs_to :cart
before_save :combine_and_reject
def combine_and_reject
#pseudo-code
#if self.sku == to other items' sku in cart
#combine the quantities and reject self silently.
#end
end
end
Thanks in advance.
Maybe I miss something but I don't understand why you want to handle this within your model. I would recommend you to calculate this "on the fly" when you display your Cart. Imagine the following code:
#carts controller
def show
skus = #cart.items.pluck(:sku)
# ['678TYU', '678TYU', 'POPO90']
skus.each_with_object(Hash.new(0)) { |sku,counts| counts[sky] += 1 }
# {"678TYU"=>2, "POPO90"=>1}
end
In this way each time you want to display your cart you can handle quantities depending of your duplicates.
Duplicates in your cart are not an issue, because in real life you can have two chocolate bar in your cart. It's only on your receipt where the duplicates disappears.
I've set up three models: User, List, and UserList -- the latter being the join model between User and List, in a has_many_through relationship.
I'm trying to set up what I think should be fairly vanilla uniqueness constraints -- but it's not quite working. Would appreciate your guidance / advice please!
Technical details
I have 3 models:
class User < ApplicationRecord
has_many :user_lists
has_many :lists, through: :user_lists, dependent: :destroy
End
class List < ApplicationRecord
has_many :user_lists
has_many :users, through: :user_lists, dependent: :destroy
# no duplicate titles in the List table
validates :title, uniqueness: true
End
class UserList < ApplicationRecord
belongs_to :list
belongs_to :user
# a given user can only have one copy of a list item
validates :list_id, uniqueness: { scope: :user_id }
end
As you can see, I'd like List items to be unique, based on their title. In other words, if user Adam adds a List with title "The Dark Knight", then user Beatrice adding a List with title "The Dark Knight" shouldn't actually create a new List record -- it should just create a new / distinct UserList association, pointing to the previously created List item.
(Somewhat tangential, but I also added a unique index on the table since I understand this avoids a race condition)
class AddIndexToUserLists < ActiveRecord::Migration[5.2]
def change
add_index :user_lists, [:user_id, :list_id], unique: true
end
end
Here's where things are going wrong.
As user Adam, I log in, and add a new title, "The Dark Knight", to my list.
Here's the controller action (assume current_user correctly retrieves Adam):
# POST /lists
def create
#list = current_user.lists.find_or_create_by!(list_params)
end
This correctly results in a new List record, and associated UserList record, being created. Hurrah!
As Adam, if I try to add that same title "The Dark Knight", to my list again, nothing happens -- including no errors on the console. Hurrah!
However -- as user Beatrice, if I log in and now try to add "The Dark Knight" to my list, I now get an error in the console:
POST http://localhost:3000/api/v1/lists 422 (Unprocessable Entity)
My debugging and hypothesis
If I remove the uniqueness constraint on List.title, this error disappears, and Beatrice is able to add "The Dark Knight" to her list.
However, List then contains two records, both titled "The Dark Knight", which seems redundant.
As Adam, it seems like perhaps current_user.lists.find_or_create_by!(list_params) in my controller action is finding the existing "The Dark Knight" list associated with my current user, and realising it exists -- thereby not triggering the create action.
Then as Beatrice, it seems that the same controller action is not finding the existing "The Dark Knight" list item associated with my current user -- and therefore it tries to trigger the create action.
However, this create action tries to create a new List item with a title that already exists -- i.e. it falls foul of the List.rb model uniqueness validation.
I'm not sure how to modify that find_or_create_by action, or the model validations, to ensure that for Beatrice, a new UserList record / association is created -- but not a new List record (since that already exists).
It feels like maybe I'm missing something easy here. Or maybe not. Would really appreciate some guidance on how to proceed. Thanks!
I'm 99% certain that what's happening is current_user.lists.find_or_create_by will only search for List records that the user has a UserList entry for. Thus if the List exists but the current user doesn't have an association to it, it will try to create a new list which will conflict with the existing one.
Assuming this is the issue, you need to find the List independently of the user associations: #list = List.find_or_create_by(list_params)
Once you have that list, you can create a UserList record through the associations or the UserList model. If you're looking for brevity, I think you can use current_user.lists << #list to create the UserList, but you should check how this behaves if the user has a UserList for that list already, I'm not sure if it will overwrite your existing data.
So (assuming the << method works appropriately for creating the UserList) your controller action could look like this:
def create
#list = List.find_or_create_by!(list_params)
current_user.lists << #list
end
I've got a model that looks like this:
class thing < ActiveRecord::Base
has_many :bobbles
validate :has_two_bobbles
def has_two_bobbles
unless self.bobbles.size == 2
errors.add(:bobbles, "Need two bobbles")
end
end
end
I'm running in to trouble when updating from a form. If I delete a bobble and add a bobble in the same submission, when I hit self.bobbles.size I get 3 and not 2. Is there anywhere to restrict self.bobbles to return only the records that arent's scheduled for deletion?
I know in the controller you have access to _destroy in the params, but is there anything at the model level that indicates if a record is going to be deleted?
Record is going to die when it responded to .marked_for_destruction?
class thing < ActiveRecord::Base
has_many :bobbles
validate :has_two_bobbles
def has_two_bobbles
unless self.bobbles.select {|t| !t.marked_for_destruction?}.size == 2
errors.add(:bobbles, "Need two bobbles")
end
end
end
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