Add basic product alternatives

This commit is contained in:
Nathan Chapman 2022-04-28 20:16:39 -06:00
parent a758af854a
commit 7f4ac4ba6a
11 changed files with 234 additions and 88 deletions

View File

@ -25,6 +25,7 @@ usps-api = "*"
[dev-packages]
django-debug-toolbar = "*"
selenium = "*"
pycodestyle = "*"
[requires]
python_version = "3.10"

14
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "0087f9e4fd44233bc6329f7a844a96ae4001ee9d173323e7a715c7295844163c"
"sha256": "0847a2b4a481c279572830e8044273e65178f30b74b868549ae41e3aa35b2d03"
},
"pipfile-spec": 6,
"requires": {
@ -981,6 +981,14 @@
"markers": "python_version >= '3.6'",
"version": "==1.1.0"
},
"pycodestyle": {
"hashes": [
"sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20",
"sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"
],
"index": "pypi",
"version": "==2.8.0"
},
"pycparser": {
"hashes": [
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
@ -1039,7 +1047,7 @@
"sha256:670a52d3115d0e879e1ac838a4eb999af32f858163e3a704fe4839de2a676070",
"sha256:fb2d48e4eab0dfb786a472cd514aaadc71e3445b203bc300bad93daa75d77c1a"
],
"markers": "python_full_version >= '3.7.0'",
"markers": "python_version >= '3.7'",
"version": "==0.20.0"
},
"trio-websocket": {
@ -1063,7 +1071,7 @@
"sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b",
"sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8"
],
"markers": "python_full_version >= '3.7.0'",
"markers": "python_version >= '3.7'",
"version": "==1.1.0"
}
}

View File

@ -95,3 +95,26 @@ class ShippingContainer:
(REGIONAL_RATE_BOX_B, "Regional Rate Box B"),
(VARIABLE, "Variable")
]
class CoffeeGrind:
WHOLE = 'whole-beans'
ESPRESSO = 'espresso'
CONE_DRIP = 'cone-drip'
BASKET_DRIP = 'basket-drip'
FRENCH_PRESS = 'french-press'
STOVETOP_ESPRESSO = 'stovetop-espresso'
AEROPRESS = 'aeropress'
PERCOLATOR = 'percolator'
CAFE_STYLE = 'cafe-style'
GRIND_CHOICES = [
(WHOLE, 'Whole Beans'),
(ESPRESSO, 'Espresso'),
(CONE_DRIP, 'Cone Drip'),
(BASKET_DRIP, 'Basket Drip'),
(FRENCH_PRESS, 'French Press'),
(STOVETOP_ESPRESSO, 'Stovetop Espresso (Moka Pot)'),
(AEROPRESS, 'AeroPress'),
(PERCOLATOR, 'Percolator'),
(CAFE_STYLE, 'BLTC cafe pour over')
]

View File

@ -734,7 +734,7 @@ article + article {
justify-self: end;
}
.item__form p,
.item__form,
.coupon__form p {
display: flex;
align-items: center;

View File

@ -1,6 +1,7 @@
import logging
from decimal import Decimal
from measurement.measures import Weight
from django.conf import settings
from django.contrib import messages
from django.shortcuts import redirect, reverse
@ -36,15 +37,20 @@ class Cart:
product_id = str(product.id)
if product_id not in self.cart:
self.cart[product_id] = {
'quantity': 0,
'grind': grind,
'variations': {},
'price': str(product.price)
}
self.cart[product_id]['variations'][grind] = {'quantity': 0}
if update_quantity:
self.cart[product_id]['quantity'] = quantity
self.cart[product_id]['variations'][grind]['quantity'] = quantity
else:
self.cart[product_id]['quantity'] += quantity
if not grind in self.cart[product_id]['variations']:
# create it
self.cart[product_id]['variations'][grind] = {'quantity': quantity}
else:
# add to it
self.cart[product_id]['variations'][grind]['quantity'] += quantity
if len(self) <= 20:
self.save()
else:
@ -69,15 +75,31 @@ class Cart:
for item in self.cart.values():
item['price'] = Decimal(item['price'])
item['total_price'] = item['price'] * item['quantity']
item['total_price'] = Decimal(sum(self.get_item_prices()))
item['quantity'] = self.get_single_item_total_quantity(item)
yield item
def __len__(self):
return sum(item['quantity'] for item in self.cart.values())
return sum(self.get_all_item_quantities())
def get_all_item_quantities(self):
for item in self.cart.values():
yield sum([value['quantity'] for value in item['variations'].values()])
def get_single_item_total_quantity(self, item):
return sum([value['quantity'] for value in item['variations'].values()])
def get_item_prices(self):
for item in self.cart.values():
yield Decimal(item['price']) * sum([value['quantity'] for value in item['variations'].values()])
def get_total_price(self):
return sum(self.get_item_prices())
def get_total_weight(self):
if len(self) > 0:
return sum([item['product'].weight.value * item['quantity'] for item in self])
for item in self:
return item['product'].weight.value * sum(self.get_all_item_quantities())
else:
return 0
@ -106,9 +128,6 @@ class Cart:
else:
return Decimal('0.00')
def get_total_price(self):
return sum(Decimal(item['price']) * item['quantity'] for item in self.cart.values())
def clear(self):
del self.session[settings.CART_SESSION_ID]
try:
@ -156,7 +175,7 @@ class Cart:
bulk_list = [OrderLine(
order=order,
product=item['product'],
customer_note=item['grind'],
customer_note=f"{item['variations']}",
unit_price=item['price'],
quantity=item['quantity'],
tax_rate=2,

View File

@ -3,6 +3,7 @@ from django import forms
from django.core.mail import EmailMessage
from core.models import Order
from core import CoffeeGrind
from accounts import STATE_CHOICES
from .tasks import contact_form_email
@ -10,28 +11,7 @@ from .tasks import contact_form_email
logger = logging.getLogger(__name__)
class AddToCartForm(forms.Form):
WHOLE = 'Whole Beans'
ESPRESSO = 'Espresso'
CONE_DRIP = 'Cone Drip'
BASKET_DRIP = 'Basket Drip'
FRENCH_PRESS = 'French Press'
STOVETOP_ESPRESSO = 'Stovetop Espresso (Moka Pot)'
AEROPRESS = 'AeroPress'
PERCOLATOR = 'Percolator'
CAFE_STYLE = 'BLTC cafe pour over'
GRIND_CHOICES = [
(WHOLE, 'Whole Beans'),
(ESPRESSO, 'Espresso'),
(CONE_DRIP, 'Cone Drip'),
(BASKET_DRIP, 'Basket Drip'),
(FRENCH_PRESS, 'French Press'),
(STOVETOP_ESPRESSO, 'Stovetop Espresso (Moka Pot)'),
(AEROPRESS, 'AeroPress'),
(PERCOLATOR, 'Percolator'),
(CAFE_STYLE, 'BLTC cafe pour over')
]
grind = forms.ChoiceField(choices=GRIND_CHOICES)
grind = forms.ChoiceField(choices=CoffeeGrind.GRIND_CHOICES)
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
class UpdateCartItemForm(forms.Form):
@ -40,27 +20,6 @@ class UpdateCartItemForm(forms.Form):
class AddToSubscriptionForm(forms.Form):
WHOLE = 'Whole Beans'
ESPRESSO = 'Espresso'
CONE_DRIP = 'Cone Drip'
BASKET_DRIP = 'Basket Drip'
FRENCH_PRESS = 'French Press'
STOVETOP_ESPRESSO = 'Stovetop Espresso (Moka Pot)'
AEROPRESS = 'AeroPress'
PERCOLATOR = 'Percolator'
CAFE_STYLE = 'BLTC cafe pour over'
GRIND_CHOICES = [
(WHOLE, 'Whole Beans'),
(ESPRESSO, 'Espresso'),
(CONE_DRIP, 'Cone Drip'),
(BASKET_DRIP, 'Basket Drip'),
(FRENCH_PRESS, 'French Press'),
(STOVETOP_ESPRESSO, 'Stovetop Espresso (Moka Pot)'),
(AEROPRESS, 'AeroPress'),
(PERCOLATOR, 'Percolator'),
(CAFE_STYLE, 'BLTC cafe pour over')
]
SEVEN_DAYS = 7
FOURTEEN_DAYS = 14
THIRTY_DAYS = 30
@ -71,7 +30,7 @@ class AddToSubscriptionForm(forms.Form):
]
quantity = forms.IntegerField(min_value=1, initial=1)
grind = forms.ChoiceField(choices=GRIND_CHOICES)
grind = forms.ChoiceField(choices=CoffeeGrind.GRIND_CHOICES)
schedule = forms.ChoiceField(choices=SCHEDULE_CHOICES)

View File

@ -15,13 +15,15 @@
<div class="item__info">
<h4>{{product.name}}</h4>
<p><strong>Grind</strong>: {{item.grind}}</p>
<form class="item__form" action="{% url 'storefront:cart-update' product.pk %}" method="POST">
{% csrf_token %}
<p>
{{ item.update_quantity_form }}
<input type="submit" value="Update">
{% for key, value in item.variations.items %}
<p><strong>{{key}}</strong><br>
<form class="item__form" action="{% url 'storefront:cart-update' product.pk key %}" method="POST">
{% csrf_token %}
{{ value.update_quantity_form }}
<input type="submit" value="Update">
</form>
</p>
</form>
{% endfor %}
<p>
<a href="{% url 'storefront:cart-remove' product.pk %}">Remove from cart</a>
</p>

View File

@ -34,8 +34,9 @@
</figure>
<div>
<h4>{{product.name}}</h4>
<p><strong>Grind options</strong>: {{item.grind}}</p>
<p><strong>Quantity</strong>: {{item.quantity}}</p>
{% for key, value in item.variations.items %}
<p>Grind: <strong>{{key}}</strong>, Qty: <strong>{{value.quantity}}</strong></p>
{% endfor %}
</div>
<div class="item__price">
<p><strong>${{item.price}}</strong></p>

View File

@ -1,8 +1,10 @@
import logging
from decimal import Decimal
from django.test import TestCase, Client, RequestFactory
from django.urls import reverse
from django.conf import settings
from measurement.measures import Weight
from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
@ -14,19 +16,30 @@ from .cart import Cart
logger = logging.getLogger(__name__)
WHOLE = 'whole-beans'
ESPRESSO = 'espresso'
CONE_DRIP = 'cone-drip'
BASKET_DRIP = 'basket-drip'
FRENCH_PRESS = 'french-press'
STOVETOP_ESPRESSO = 'stovetop-espresso'
AEROPRESS = 'aeropress'
PERCOLATOR = 'percolator'
CAFE_STYLE = 'cafe-style'
class CartTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.customer = User.objects.create_user(
username='Peter Templer', email='peter@testing.com', password='peterspassword321'
username='petertempler', email='peter@testing.com', password='peterspassword321'
)
self.product = Product.objects.create(
name='Dante\'s Tornado',
description='Coffee',
sku='23987',
price=13.4,
weight=10,
weight=Weight(oz=16),
visible_in_listings=True
)
self.order = Order.objects.create(
@ -44,13 +57,12 @@ class CartTest(TestCase):
request = response.wsgi_request
cart = Cart(request)
cart.add(
request=request,
request,
product=self.product,
quantity=1,
grind=WHOLE,
update_quantity=False
)
logger.info(f'Body data:\n{body_data}\n')
params = {
'email': 'nathanchapman@hey.com',
@ -60,3 +72,129 @@ class CartTest(TestCase):
}
response = self.client.post(url, params)
self.assertContains(response, 'Checkout', status_code=200)
def test_cart_item_variations(self):
cart_detail_url = reverse('storefront:cart-detail')
response = self.client.get(cart_detail_url)
request = response.wsgi_request
cart = Cart(request)
cart = Cart(request)
cart.add(
request,
product=self.product,
quantity=1,
grind=WHOLE,
update_quantity=False
)
cart.add(
request,
product=self.product,
quantity=1,
grind=ESPRESSO,
update_quantity=False
)
for item in cart.cart.values():
self.assertTrue('variations' in item, item)
def test_add_item_to_cart(self):
cart_detail_url = reverse('storefront:cart-detail')
response = self.client.get(cart_detail_url)
request = response.wsgi_request
cart = Cart(request)
cart = Cart(request)
cart.add(
request,
product=self.product,
quantity=1,
grind=WHOLE,
update_quantity=False
)
self.assertEqual(cart.cart[f'{self.product.id}']['variations'][WHOLE]['quantity'], 1)
self.assertEqual(len(cart), 1)
self.assertEqual(sum(cart.get_item_prices()), Decimal('13.4'))
self.assertEqual(cart.get_total_price(), Decimal('13.4'))
cart.add(
request,
product=self.product,
quantity=1,
grind=WHOLE,
update_quantity=False
)
self.assertEqual(cart.cart[f'{self.product.id}']['variations'][WHOLE]['quantity'], 2)
self.assertEqual(len(cart), 2)
cart.add(
request,
product=self.product,
quantity=3,
grind=ESPRESSO,
update_quantity=False
)
self.assertEqual(cart.cart[f'{self.product.id}']['variations'][ESPRESSO]['quantity'], 3)
self.assertEqual(len(cart), 5)
self.assertEqual(cart.get_total_price(), Decimal('67'))
def test_update_cart_item_quantity(self):
cart_detail_url = reverse('storefront:cart-detail')
response = self.client.get(cart_detail_url)
request = response.wsgi_request
cart = Cart(request)
cart = Cart(request)
cart.add(
request,
product=self.product,
quantity=3,
grind=WHOLE,
update_quantity=False
)
self.assertEqual(cart.cart[f'{self.product.id}']['variations'][WHOLE]['quantity'], 3)
cart.add(
request,
product=self.product,
quantity=1,
grind=WHOLE,
update_quantity=True
)
self.assertEqual(cart.cart[f'{self.product.id}']['variations'][WHOLE]['quantity'], 1)
def test_cart_remove_item(self):
cart_detail_url = reverse('storefront:cart-detail')
response = self.client.get(cart_detail_url)
request = response.wsgi_request
cart = Cart(request)
cart = Cart(request)
cart.add(
request,
product=self.product,
quantity=3,
grind=WHOLE,
update_quantity=False
)
self.assertEqual(len(cart), 3)
cart.remove(self.product)
self.assertEqual(len(cart), 0)
def test_cart_get_total_weight(self):
cart_detail_url = reverse('storefront:cart-detail')
response = self.client.get(cart_detail_url)
request = response.wsgi_request
cart = Cart(request)
cart = Cart(request)
cart.add(
request,
product=self.product,
quantity=3,
grind=WHOLE,
update_quantity=False
)
self.assertEqual(cart.get_total_weight(), Decimal(48))
# 96oz

View File

@ -14,7 +14,7 @@ urlpatterns = [
path('cart/', views.CartView.as_view(), name='cart-detail'),
path('cart/<int:pk>/add/', views.CartAddProductView.as_view(), name='cart-add'),
path('cart/<int:pk>/update/', views.CartUpdateProductView.as_view(), name='cart-update'),
path('cart/<int:pk>/update/<slug:grind>/', views.CartUpdateProductView.as_view(), name='cart-update'),
path('cart/<int:pk>/remove/', views.cart_remove_product_view, name='cart-remove'),
path('coupon/apply/', views.CouponApplyView.as_view(), name='coupon-apply'),

View File

@ -42,11 +42,12 @@ class CartView(TemplateView):
context = super().get_context_data(**kwargs)
cart = Cart(self.request)
for item in cart:
item['update_quantity_form'] = UpdateCartItemForm(
initial={
'quantity': item['quantity'],
}
)
for variation in item['variations'].values():
variation['update_quantity_form'] = UpdateCartItemForm(
initial={
'quantity': variation['quantity']
}
)
context['cart'] = cart
context['coupon_apply_form'] = CouponApplyForm()
return context
@ -90,6 +91,7 @@ class CartUpdateProductView(SingleObjectMixin, FormView):
cart.add(
request=request,
product=self.get_object(),
grind=kwargs['grind'],
quantity=form.cleaned_data['quantity'],
update_quantity=form.cleaned_data['update']
)
@ -198,14 +200,7 @@ class OrderCreateView(CreateView):
'shipping_total': cart.get_shipping_cost()
}
if self.request.user.is_authenticated:
user_info = {
'email': self.request.user.email,
'first_name': self.request.user.first_name,
'last_name': self.request.user.last_name,
}
initial |= user_info
elif self.request.session.get('shipping_address'):
if self.request.session.get('shipping_address'):
a = self.request.session.get('shipping_address')
user_info = {
'email': a['email'],