Merge branch 'release/1.0.7'

This commit is contained in:
Nathan Chapman 2022-04-28 20:16:54 -06:00
commit ff1df9827a
11 changed files with 234 additions and 88 deletions

View File

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

14
Pipfile.lock generated
View File

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

View File

@ -95,3 +95,26 @@ class ShippingContainer:
(REGIONAL_RATE_BOX_B, "Regional Rate Box B"), (REGIONAL_RATE_BOX_B, "Regional Rate Box B"),
(VARIABLE, "Variable") (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; justify-self: end;
} }
.item__form p, .item__form,
.coupon__form p { .coupon__form p {
display: flex; display: flex;
align-items: center; align-items: center;

View File

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

View File

@ -3,6 +3,7 @@ from django import forms
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from core.models import Order from core.models import Order
from core import CoffeeGrind
from accounts import STATE_CHOICES from accounts import STATE_CHOICES
from .tasks import contact_form_email from .tasks import contact_form_email
@ -10,28 +11,7 @@ from .tasks import contact_form_email
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AddToCartForm(forms.Form): class AddToCartForm(forms.Form):
WHOLE = 'Whole Beans' grind = forms.ChoiceField(choices=CoffeeGrind.GRIND_CHOICES)
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)
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1) quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
class UpdateCartItemForm(forms.Form): class UpdateCartItemForm(forms.Form):
@ -40,27 +20,6 @@ class UpdateCartItemForm(forms.Form):
class AddToSubscriptionForm(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 SEVEN_DAYS = 7
FOURTEEN_DAYS = 14 FOURTEEN_DAYS = 14
THIRTY_DAYS = 30 THIRTY_DAYS = 30
@ -71,7 +30,7 @@ class AddToSubscriptionForm(forms.Form):
] ]
quantity = forms.IntegerField(min_value=1, initial=1) 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) schedule = forms.ChoiceField(choices=SCHEDULE_CHOICES)

View File

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

View File

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

View File

@ -1,8 +1,10 @@
import logging import logging
from decimal import Decimal
from django.test import TestCase, Client, RequestFactory from django.test import TestCase, Client, RequestFactory
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from measurement.measures import Weight
from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
@ -14,19 +16,30 @@ from .cart import Cart
logger = logging.getLogger(__name__) 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): class CartTest(TestCase):
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
self.factory = RequestFactory()
self.customer = User.objects.create_user( 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( self.product = Product.objects.create(
name='Dante\'s Tornado', name='Dante\'s Tornado',
description='Coffee', description='Coffee',
sku='23987', sku='23987',
price=13.4, price=13.4,
weight=10, weight=Weight(oz=16),
visible_in_listings=True visible_in_listings=True
) )
self.order = Order.objects.create( self.order = Order.objects.create(
@ -44,13 +57,12 @@ class CartTest(TestCase):
request = response.wsgi_request request = response.wsgi_request
cart = Cart(request) cart = Cart(request)
cart.add( cart.add(
request=request, request,
product=self.product, product=self.product,
quantity=1, quantity=1,
grind=WHOLE,
update_quantity=False update_quantity=False
) )
logger.info(f'Body data:\n{body_data}\n')
params = { params = {
'email': 'nathanchapman@hey.com', 'email': 'nathanchapman@hey.com',
@ -60,3 +72,129 @@ class CartTest(TestCase):
} }
response = self.client.post(url, params) 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/', views.CartView.as_view(), name='cart-detail'),
path('cart/<int:pk>/add/', views.CartAddProductView.as_view(), name='cart-add'), 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('cart/<int:pk>/remove/', views.cart_remove_product_view, name='cart-remove'),
path('coupon/apply/', views.CouponApplyView.as_view(), name='coupon-apply'), path('coupon/apply/', views.CouponApplyView.as_view(), name='coupon-apply'),

View File

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