Spree Commerce: Associate line items in the cart? - ruby-on-rails

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

Related

Rails 5.2 - how to setup a relationship between a parent and a typed child model?

I have a basic model structure like this:
Template - has_many :cards
Card - belongs_to :template, optional: true
When a Card is created, the user selects a Template and upon saving it, the template_type is set to 'list'.
A Card can only have one 'list'.
How can I create a relationship that will give me a method on the Template model that will allow, given an instance of a card, that I can do this:
my_card.list
and from a given template instance (that is of template_type 'list'):
this_template.card
I think the current problem is that the column in the template model is called template_type when perhaps it should have been just called type. It is implemented as an enum:
enum template_type: { template: 0, list: 1 }
But also I need to define the typed relationship on the models.
What I have tried
Card.rb
belongs_to :list, class_name: 'Template', optional: true, inverse_of: :owner
Template.rb
belongs_to :owner, -> { where.template_type[:list] }, inverse_of: :lists
Although that allows me to do
my_card.list
it is null (even though my_card.template returns a template with template_type of 'checklist').
I am guessing that would need a migration to add owner_id to the template table. But this does not seem correct as the card table already as a field called 'template_id', so all I need is for Rails to use that to access the template (of type 'list') that belongs to it, through a method called checklist. And to do the reverse from a template (of type 'checklist') to find its 'owning' card.
As a second question, how can I enforce a rule that a card can only have one Template of type 'list'?
EDIT
I also read this question and wonder if I should setup a separate model called List that encapsulates the domain better?
UPDATE in response to comment
A list is structurally identical to a template except it can be 'completed' whereas a template cannot. The user can create as many lists as they like from a single template, and each list is linked to a card which keeps track of what they do with the list.
There was no point creating a separate table for a list, since it is basically identical to a template other than that (it has a completed or not completed status flag, which is a filed in the template table, but a template_type of type template will never use it).
For my 1st question, I had to tell Rails to use the existing foreign key:
card.rb
belongs_to :list, class_name: 'Template', foreign_key: 'template_id'
template.rb
has_one :card, inverse_of: :list
Now I can reference a card from a list and a list from a card. No database changes needed. :)
Thanks to #CannonMoyer I managed to add a custom validation for my 2nd question:
validate :template_is_a_list
def template_is_a_list
errors.add(:template, 'must be of type list') unless
template.template_type == Template.template_types[:list]
end

Guidance on how to set validations correctly with a has_many :through relationship?

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

What is the best way to model a restaurant menu, capable of future editing?

I am creating a website with Active Admin to allow the owners future control over the menu. If we are working on the lunch menu, I have structured the models as a section that has_many items shown below.
class Section < ActiveRecord::Base
attr_accessible :id, :name
has_many :items
end
class Item < ActiveRecord::Base
attr_accessible :desc, :id, :name, :price
belongs_to :section
end
Creating a "section" works fine in Active Admin, but I am receiving an error in when trying to create an "item".
Error:
NoMethodError in Admin/items#new
undefined method `section_id' for #<Item:0xb5460b44>
Thanks.
For the sake of flexibility you should consider not creating categories as models but as mere attributes. Once you create all the classes(ex. LunchSection, DinnerSection, WineSection) it's impossible to create a new one without programmer's participation.
What I would go for is create following classes:
Menu - representing a menu as in a separate piece of paper (so there could be a wine menu and dish menu for ex.). The "type" of the menu should be designated only by name.
Group or Category is a container for diffrent dishes of the same type like lunch, dessert, wine... as wel, theres just one Group class and there's an instance for each single group
MenuItem (or simply Item) is any element of a menu, belonging to group or menu (this is a design decision - items should not belong to both menu and group because that would cause conflict if you wanted to get all items in a menu)

Customizing active_admin interface

I've a simple question about active admin interface.
In my application, I've a resource added to the active_admin. When I access the resource from active_admin, I get all records for that resource. When I select/access (as a show action) one record it shows details of that instance and all belongs_to associations but I don't know how to get the has_many or has_one association details in the show view?
Any ideas? I appreciate any feedback.
Thanks,
Atarang.
You need to customize your show screen in app/admin/yourresource.rb. You shouldn't need to do anything special otherwise, other than making sure the has_many and belongs_to associations are correct. For example, if you have a category with many items, do this in category.rb:
show :category do
panel "Category Info" do
attributes_table_for category, :name, :created_at
end
panel "Items in This Category" do
table_for(category.items) do
column("Name", :sortable => :name) {|item| item.name }
column("Created At") {|item| item.created_at }
end
end
end
There are more good examples here and elsewhere in the source for the demo project, which for some reason is hard to find from the main site.

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

Resources