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.
Related
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
Context:
Each Order has many Items & Logistics. Each Item & Logistic (as well as the Order itself) have many Revenues.
I am creating Order + Items & Logistics at once using an accepts_nested_attributes_for on Order. However, Revenues gets created using an after_create callback on each of the models Order, Item, and Logistics. Why? Because given the difference in interpretation in these models, the code reads cleaner this way. (But if this way of doing it is what's causing this question to be asked, I will obviously reconsider!)
One key attribute that I need to store in Revenues is pp_charge_id. But pp_charge_id is not something that either Order, Items, or Logistics needs to worry about. I've attached an attr_accessor :pp_charge_id to Order, so that one works fine, however, once I'm in the child Items or Logistics models, I no longer have access to pp_charge_id which again I need to save an associated Revenue. How should I do this?
Controller Code:
#order = Order.new(params) #params includes Order params, and nested params for child Item & Logistics
#order.pp_charge_id = "cash"
#order.save #I need this to not only save the Order, the children Item & Logistics, but then to also create the associated Revenue for each of the aforementioned 3 models
ORDER Model Code:
has_many :items
has_many :revenues
attr_accessor :pp_charge_id
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.pp_charge_id)
end
#This WORKS as expected because of the attr_accessor
ITEM/ LOGISTIC model code:
has_many :revenues
belongs_to :order
after_create :create_revenue
def create_revenue
self.revenues.create(pp_charge_id: self.order.pp_charge_id)
end
#This DOES NOT work because self.order.pp_charge_id is nil
ORDER model code:
belongs_to :order
belongs_to :item
belongs_to :logistic
Again I understand the attr_accessor is not designed to persist across a request or even if the Order itself is reloaded. But it also doesn't make sense to save it redundantly in a table that has no use for it. If the only way to do this is to put the pp_charge_id into the params for the order and save everything all at once (including Revenues), then let me know because I know how to do that. (Again, would just rather avoid that because of how it's interpreted: params are coming from User, Revenue data is something I'm providing)
I think if you want the order's pp_charge_id to apply to all its items and logistics, I'd put all that into the order's after_create callback:
# order.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id)
items.each {|i| i.revenues.create(pp_charge_id: pp_charge_id)}
logistics.each {|l| l.revenues.create(pp_charge_id: pp_charge_id)}
end
EDIT: Alternately, you could add inverse_of to your belongs_to declarations, and then I believe Item#create_revenue would see the same Order instance that you set in the controller. So if you also added an attr_accessor to the Item class, you could write its create_revenue like this:
# item.rb
def create_revenue
revenues.create(pp_charge_id: pp_charge_id || order.pp_charge_id)
end
This should cover the new requirement you've mentioned in your comment.
instead of using after_create and accessors you should consider having a proper method that does exactly what you need, ie:
Order.create_with_charge(:cash, params)
i find it disturbing to persist redundant information in the database just because the code reads cleaner that way!
I have about 12 models but I'm not sure how to make relations and another stuff with 3 model:
Current relationships:
purchase has_many purchase_items
purchase_item belongs_to item
purchase_item belongs_to purchase
item has_many purchase_items
I've made working form, where I can create new purchase (add multiple items), but now i want to store in table 'inventory' actual quantity of items, so after create/update/delete model purchase or purchase_items (I'm not sure which) model 'inventory' should be updated also. I know that I have to make this in ActiveRecord callback.
Questions:
Which relation should I use to make that and between which models?
Current snippet of code in view (new purchase):
<%= purchase_form.nested_fields_for :purchase_items do |nested| %>
Do I have to add something to this ^ for model inventory?
You wouldn't necessarily need to update your inventory model to accomplish this. Consider solving it like this:
# purchase_item.rb
after_save :update_quantity
def update_quantity
item.quantity --
end
And accessing the inventory for each specific item could be something like this:
# inventory.rb
def get_inventory_for_item(item)
item.quantity
end
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 am implementing an online application shop with Rails. Its data model is shown as follows:
class User < ActiveRecord::Base
has_many purchase_records
has_many items, :through => purchase_records
end
class Item < ActiveRecord::Base
has_many purchase_records
has_many users, :through => purchase_records
end
class PurchaseRecord < ActiveRecord::Base
belongs_to :user
belongs_to :item
end
It has a page showing available items and there prices, and if the user has purchased the item, the price will be a download link, just as App Store does. A view helper is written to help generate such links:
def download_link(item)
# generate a download link
end
def item_link(item)
if current_user and current_user.items.where(:id => item.id).first != nil
# User already purchased it
download_link(item, 'book-price')
else
# Not purchased yet, show price and link to its details
link_to item.price, item
end
end
current_user is defined by devise. It works fine except for it costs 20 extra database queries for a page with 20 items, since it needs to check if the user has purchased the item or not for every single item. I am wondering if it can be optimized, for example, to pre-load purchased items of current user, but I have no idea how to write it in a view helper.
I just implemented downloadable content for a client.
What I did was write an instance method on the user class that retrieves the user's purchased items, e.g.:
class User < ActiveRecord::Base
...
def downloads
self.orders.collect { |o| o.products }.flatten
end
end
You could use the include? method to check if the user purchased the item, e.g.:
def item_link(item)
if current_user && current_user.downloads.include?(item)
download_link(item, 'book-price')
else
link_to item.price, item
end
end
Unfortunately, while this is a bit more explicit, it will still loop through the user's orders every time item_link is hit. I would suggest optimizing this with Rails low-level caching where you may clear the cache every time the user logs in or completes a purchase.
A Rails low-level cache may look like this:
def downoads
Rails.cache.fetch("user-downloads-#{self.id}") do
self.orders.collect { |o| o.products }.flatten
end
end
And call the following to clear a cache:
Rails.cache.delete("user-downloads-#{self.id}")
You could set the user's purchased items to an instance variable in the controller. Then you're only hitting the database once:
# app/controllers/items_controller.rb
def index
#purchased_items = current_user.items
end
# app/helpers/items_helper.rb
def item_link(item)
if #purchased_items.include?(item)
download_link(...)
else
link_to ...
end
end
Well, you don't write that in a view helper. You make a scope on the user model called purchased_items where you would check all of the items a user has purchased.
Since you didn't put up the source code for User, Item and whatever their relationship is, I can only give you that general hint.