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
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'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 am writing a Spree extension to allow certain items in the cart/order to be linked to each other.
A "setting" product can be associated with a "center stone" product. Eventually, there shall be constraints enforcing which things can reference each other, but that's not important yet.
Here's how I've changed the LineItem to include self references:
Spree::LineItem.class_eval do
has_one :center_stone, class_name: "LineItem", foreign_key: "setting_id"
belongs_to :setting, class_name: "LineItem"
end
...and the corresponding DB migration:
class AddSettingRefToLineItems < ActiveRecord::Migration
def change
add_reference :spree_line_items, :setting, index: true, foreign_key: true
end
end
What I need to accomplish next is to modify the "Add to Cart" form on the product page so that an item being added to the cart can get associated with an item that is already in the cart. How do I do this?
Use Case Example
Product A and Product B are both in my cart. I am looking at the page for Product C. I want to see the options:
Add to Product A
Add to Product B
Add to Cart Alone
Clicking any of these options creates a Spree::LineItem for Product C as usual. If click the first two option, I also want the LineItem for Product C's setting_id to reference the LineItem for Product A in my cart.
As was discovered, the main questions is: how to customize Spree's "add to card" function.
You need to customize views:
Deface::Override.new(:virtual_path => 'spree/products/_cart_form',
:name => 'centerproduct_cart_form',
:replace => "<what to replace>",
:erb => "<with what to replace>")
This should go to your app/overrides/centerproduct_cart_form.rb file (you can change name of file, just make sure name parameter in the code sample above will be changed as well to the same value).
You can figure out what to replace and with what to replace parts by looking at the source code of the view:
https://github.com/spree/spree/blob/master/frontend/app/views/spree/products/_cart_form.html.erb
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
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.