diff --git a/Pipfile b/Pipfile index ab813a8..d44e94a 100644 --- a/Pipfile +++ b/Pipfile @@ -25,6 +25,7 @@ usps-api = "*" [dev-packages] django-debug-toolbar = "*" selenium = "*" +pycodestyle = "*" [requires] python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index e4b6c06..c68f1ce 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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" } } diff --git a/src/core/__init__.py b/src/core/__init__.py index 38676a8..87741ff 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -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') + ] diff --git a/src/static/styles/main.css b/src/static/styles/main.css index 8daed62..353869f 100644 --- a/src/static/styles/main.css +++ b/src/static/styles/main.css @@ -734,7 +734,7 @@ article + article { justify-self: end; } -.item__form p, +.item__form, .coupon__form p { display: flex; align-items: center; diff --git a/src/storefront/cart.py b/src/storefront/cart.py index 2ffea70..0cb41b8 100644 --- a/src/storefront/cart.py +++ b/src/storefront/cart.py @@ -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, diff --git a/src/storefront/forms.py b/src/storefront/forms.py index 0dedc5d..cfa559c 100644 --- a/src/storefront/forms.py +++ b/src/storefront/forms.py @@ -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) diff --git a/src/storefront/templates/storefront/cart_detail.html b/src/storefront/templates/storefront/cart_detail.html index 41353c8..2526911 100644 --- a/src/storefront/templates/storefront/cart_detail.html +++ b/src/storefront/templates/storefront/cart_detail.html @@ -15,13 +15,15 @@

{{product.name}}

Grind: {{item.grind}}

-
- {% csrf_token %} -

- {{ item.update_quantity_form }} - + {% for key, value in item.variations.items %} +

{{key}}
+ + {% csrf_token %} + {{ value.update_quantity_form }} + +

- + {% endfor %}

Remove from cart

diff --git a/src/storefront/templates/storefront/order_form.html b/src/storefront/templates/storefront/order_form.html index 6880b4d..503afec 100644 --- a/src/storefront/templates/storefront/order_form.html +++ b/src/storefront/templates/storefront/order_form.html @@ -34,8 +34,9 @@

{{product.name}}

-

Grind options: {{item.grind}}

-

Quantity: {{item.quantity}}

+ {% for key, value in item.variations.items %} +

Grind: {{key}}, Qty: {{value.quantity}}

+ {% endfor %}

${{item.price}}

diff --git a/src/storefront/tests.py b/src/storefront/tests.py index 29c3f48..53482ee 100644 --- a/src/storefront/tests.py +++ b/src/storefront/tests.py @@ -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 diff --git a/src/storefront/urls.py b/src/storefront/urls.py index ce70306..bb576d8 100644 --- a/src/storefront/urls.py +++ b/src/storefront/urls.py @@ -14,7 +14,7 @@ urlpatterns = [ path('cart/', views.CartView.as_view(), name='cart-detail'), path('cart//add/', views.CartAddProductView.as_view(), name='cart-add'), - path('cart//update/', views.CartUpdateProductView.as_view(), name='cart-update'), + path('cart//update//', views.CartUpdateProductView.as_view(), name='cart-update'), path('cart//remove/', views.cart_remove_product_view, name='cart-remove'), path('coupon/apply/', views.CouponApplyView.as_view(), name='coupon-apply'), diff --git a/src/storefront/views.py b/src/storefront/views.py index 77747cc..1a47477 100644 --- a/src/storefront/views.py +++ b/src/storefront/views.py @@ -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'],