From 44e73ca790fe87d2006f394e3f059b19a32743b8 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Tue, 14 Jun 2022 10:15:20 -0600 Subject: [PATCH 1/3] Add specific shipping choice when 6 bags are in the cart --- src/core/__init__.py | 108 +++++++++--------- src/core/fixtures/coupons.json | 2 +- src/functional_tests/test_address.py | 4 +- src/functional_tests/test_coupon.py | 4 +- src/storefront/cart.py | 33 ++++-- src/storefront/forms.py | 14 ++- .../storefront/checkout_address.html | 2 +- .../storefront/checkout_shipping_form.html | 39 +++++++ src/storefront/urls.py | 5 + src/storefront/views.py | 58 +++++++++- 10 files changed, 191 insertions(+), 78 deletions(-) create mode 100644 src/storefront/templates/storefront/checkout_shipping_form.html diff --git a/src/core/__init__.py b/src/core/__init__.py index d70bcf6..16798fe 100644 --- a/src/core/__init__.py +++ b/src/core/__init__.py @@ -2,103 +2,105 @@ from django.conf import settings class DiscountValueType: - FIXED = "fixed" - PERCENTAGE = "percentage" + FIXED = 'fixed' + PERCENTAGE = 'percentage' CHOICES = [ (FIXED, settings.DEFAULT_CURRENCY), - (PERCENTAGE, "%"), + (PERCENTAGE, '%'), ] class VoucherType: - SHIPPING = "shipping" - ENTIRE_ORDER = "entire_order" - SPECIFIC_PRODUCT = "specific_product" + SHIPPING = 'shipping' + ENTIRE_ORDER = 'entire_order' + SPECIFIC_PRODUCT = 'specific_product' CHOICES = [ - (ENTIRE_ORDER, "Entire order"), - (SHIPPING, "Shipping"), - (SPECIFIC_PRODUCT, "Specific products, collections and categories"), + (ENTIRE_ORDER, 'Entire order'), + (SHIPPING, 'Shipping'), + (SPECIFIC_PRODUCT, 'Specific products, collections and categories'), ] class OrderStatus: - DRAFT = "draft" # fully editable, not finalized order created by staff users - UNFULFILLED = "unfulfilled" # order with no items marked as fulfilled + DRAFT = 'draft' # fully editable, not finalized order created by staff users + UNFULFILLED = 'unfulfilled' # order with no items marked as fulfilled PARTIALLY_FULFILLED = ( - "partially_fulfilled" # order with some items marked as fulfilled + 'partially_fulfilled' # order with some items marked as fulfilled ) - FULFILLED = "fulfilled" # order with all items marked as fulfilled + FULFILLED = 'fulfilled' # order with all items marked as fulfilled PARTIALLY_RETURNED = ( - "partially_returned" # order with some items marked as returned + 'partially_returned' # order with some items marked as returned ) - RETURNED = "returned" # order with all items marked as returned - CANCELED = "canceled" # permanently canceled order + RETURNED = 'returned' # order with all items marked as returned + CANCELED = 'canceled' # permanently canceled order CHOICES = [ - (DRAFT, "Draft"), - (UNFULFILLED, "Unfulfilled"), - (PARTIALLY_FULFILLED, "Partially fulfilled"), - (PARTIALLY_RETURNED, "Partially returned"), - (RETURNED, "Returned"), - (FULFILLED, "Fulfilled"), - (CANCELED, "Canceled"), + (DRAFT, 'Draft'), + (UNFULFILLED, 'Unfulfilled'), + (PARTIALLY_FULFILLED, 'Partially fulfilled'), + (PARTIALLY_RETURNED, 'Partially returned'), + (RETURNED, 'Returned'), + (FULFILLED, 'Fulfilled'), + (CANCELED, 'Canceled'), ] class TransactionStatus: - CREATED = "CREATED" # The order was created with the specified context. - SAVED = "SAVED" # The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order. - APPROVED = "APPROVED" # The customer approved the payment through the PayPal wallet or another form of guest or unbranded payment. For example, a card, bank account, or so on. - VOIDED = "VOIDED" # All purchase units in the order are voided. - COMPLETED = "COMPLETED" # The payment was authorized or the authorized payment was captured for the order. - PAYER_ACTION_REQUIRED = "PAYER_ACTION_REQUIRED" # The order requires an action from the payer (e.g. 3DS authentication). Redirect the payer to the "rel":"payer-action" HATEOAS link returned as part of the response prior to authorizing or capturing the order. + CREATED = 'CREATED' # The order was created with the specified context. + SAVED = 'SAVED' # The order was saved and persisted. The order status continues to be in progress until a capture is made with final_capture = true for all purchase units within the order. + APPROVED = 'APPROVED' # The customer approved the payment through the PayPal wallet or another form of guest or unbranded payment. For example, a card, bank account, or so on. + VOIDED = 'VOIDED' # All purchase units in the order are voided. + COMPLETED = 'COMPLETED' # The payment was authorized or the authorized payment was captured for the order. + PAYER_ACTION_REQUIRED = 'PAYER_ACTION_REQUIRED' # The order requires an action from the payer (e.g. 3DS authentication). Redirect the payer to the 'rel':'payer-action' HATEOAS link returned as part of the response prior to authorizing or capturing the order. CHOICES = [ - (CREATED, "Created"), - (SAVED, "Saved"), - (APPROVED, "Approved"), - (VOIDED, "Voided"), - (COMPLETED, "Completed"), - (PAYER_ACTION_REQUIRED, "Payer action required") + (CREATED, 'Created'), + (SAVED, 'Saved'), + (APPROVED, 'Approved'), + (VOIDED, 'Voided'), + (COMPLETED, 'Completed'), + (PAYER_ACTION_REQUIRED, 'Payer action required') ] class ShippingMethodType: - PRICE_BASED = "price" - WEIGHT_BASED = "weight" + PRICE_BASED = 'price' + WEIGHT_BASED = 'weight' CHOICES = [ - (PRICE_BASED, "Price based shipping"), - (WEIGHT_BASED, "Weight based shipping"), + (PRICE_BASED, 'Price based shipping'), + (WEIGHT_BASED, 'Weight based shipping'), ] class ShippingService: - FIRST_CLASS = "FIRST CLASS" - PRIORITY = "PRIORITY" - PRIORITY_COMMERCIAL = "PRIORITY COMMERCIAL" + FIRST_CLASS = 'FIRST CLASS' + PRIORITY = 'PRIORITY' + PRIORITY_COMMERCIAL = 'PRIORITY COMMERCIAL' CHOICES = [ - (FIRST_CLASS, "First Class"), - (PRIORITY, "Priority"), - (PRIORITY_COMMERCIAL, "Priority Commercial") + (FIRST_CLASS, 'First Class'), + (PRIORITY, 'Priority'), + (PRIORITY_COMMERCIAL, 'Priority Commercial') ] class ShippingContainer: - LG_FLAT_RATE_BOX = "LG FLAT RATE BOX" - REGIONAL_RATE_BOX_A = "REGIONALRATEBOXA" - REGIONAL_RATE_BOX_B = "REGIONALRATEBOXB" - VARIABLE = "VARIABLE" + LG_FLAT_RATE_BOX = 'LG FLAT RATE BOX' + MD_FLAT_RATE_BOX = 'MD FLAT RATE BOX' + REGIONAL_RATE_BOX_A = 'REGIONALRATEBOXA' + REGIONAL_RATE_BOX_B = 'REGIONALRATEBOXB' + VARIABLE = 'VARIABLE' CHOICES = [ - (LG_FLAT_RATE_BOX, "Flate Rate Box - Large"), - (REGIONAL_RATE_BOX_A, "Regional Rate Box A"), - (REGIONAL_RATE_BOX_B, "Regional Rate Box B"), - (VARIABLE, "Variable") + (LG_FLAT_RATE_BOX, 'Flate Rate Box - Large'), + (MD_FLAT_RATE_BOX, 'Flate Rate Box - Medium'), + (REGIONAL_RATE_BOX_A, 'Regional Rate Box A'), + (REGIONAL_RATE_BOX_B, 'Regional Rate Box B'), + (VARIABLE, 'Variable') ] diff --git a/src/core/fixtures/coupons.json b/src/core/fixtures/coupons.json index 966274d..62d65b6 100644 --- a/src/core/fixtures/coupons.json +++ b/src/core/fixtures/coupons.json @@ -6,7 +6,7 @@ "name": "Save 10%: Valid", "code": "MAY2022", "valid_from": "2022-05-01T06:00:00Z", - "valid_to": "2022-05-31T06:00:00Z", + "valid_to": "2054-05-31T06:00:00Z", "discount_value_type": "percentage", "discount_value": "10.00", "products": [], diff --git a/src/functional_tests/test_address.py b/src/functional_tests/test_address.py index 5608809..d06c070 100644 --- a/src/functional_tests/test_address.py +++ b/src/functional_tests/test_address.py @@ -43,7 +43,7 @@ class AddressTests(StaticLiveServerTestCase): state_select.select_by_value('UT') postal_code_input = self.browser.find_element_by_name('postal_code') postal_code_input.send_keys('37461') - self.browser.find_element_by_xpath('//input[@value="Continue to Payment"]').click() + self.browser.find_element_by_xpath('//input[@value="Continue"]').click() # try: # WebDriverWait(self.browser, 4).until( # EC.presence_of_element_located((By.CLASS_NAME, 'errorlist')) @@ -77,7 +77,7 @@ class AddressTests(StaticLiveServerTestCase): state_select.select_by_value('AK') postal_code_input = self.browser.find_element_by_name('postal_code') postal_code_input.send_keys('99801') - self.browser.find_element_by_xpath('//input[@value="Continue to Payment"]').click() + self.browser.find_element_by_xpath('//input[@value="Continue"]').click() # try: # WebDriverWait(self.browser, 4).until( # EC.presence_of_element_located((By.CLASS_NAME, 'errorlist')) diff --git a/src/functional_tests/test_coupon.py b/src/functional_tests/test_coupon.py index 97a9b0f..e5665f5 100644 --- a/src/functional_tests/test_coupon.py +++ b/src/functional_tests/test_coupon.py @@ -85,7 +85,7 @@ class CouponTests(StaticLiveServerTestCase): postal_code_input = self.browser.find_element_by_name('postal_code') postal_code_input.send_keys('84321') self.browser.find_element_by_xpath( - '//input[@value="Continue to Payment"]' + '//input[@value="Continue"]' ).click() self.assertEqual( @@ -140,7 +140,7 @@ class CouponTests(StaticLiveServerTestCase): postal_code_input = self.browser.find_element_by_name('postal_code') postal_code_input.send_keys('84321') self.browser.find_element_by_xpath( - '//input[@value="Continue to Payment"]' + '//input[@value="Continue"]' ).click() self.assertEqual( diff --git a/src/storefront/cart.py b/src/storefront/cart.py index 3a113f2..c783e96 100644 --- a/src/storefront/cart.py +++ b/src/storefront/cart.py @@ -30,6 +30,7 @@ class Cart: self.request = request self.session = request.session self.coupon_code = self.session.get('coupon_code') + self.container = self.session.get('shipping_container') cart = self.session.get(settings.CART_SESSION_ID) if not cart: cart = self.session[settings.CART_SESSION_ID] = {} @@ -109,20 +110,26 @@ class Cart: else: return 0 - def get_shipping_box(self): + def get_shipping_box(self, container=None): + if container: + return container + + if self.container: + return self.container + if len(self) > 6 and len(self) <= 10: return ShippingContainer.LG_FLAT_RATE_BOX - elif len(self) > 2 and len(self) <= 6: + elif len(self) > 3 and len(self) <= 6: return ShippingContainer.REGIONAL_RATE_BOX_B - elif len(self) <= 2: + elif len(self) <= 3: return ShippingContainer.REGIONAL_RATE_BOX_A else: return ShippingContainer.VARIABLE - def get_shipping_cost(self): + def get_shipping_cost(self, container=None): if len(self) > 0 and self.session.get("shipping_address"): try: - usps_rate_request = self.build_usps_rate_request() + usps_rate_request = self.build_usps_rate_request(container) except TypeError as e: return Decimal('0.00') usps = USPSApi(settings.USPS_USER_ID, test=True) @@ -135,7 +142,7 @@ class Cart: ) logger.info(validation.result) - if not 'Error' in validation.result['RateV4Response']['Package']: + if 'Error' not in validation.result['RateV4Response']['Package']: rate = validation.result['RateV4Response']['Package']['Postage']['CommercialRate'] else: logger.error("USPS Rate error") @@ -152,7 +159,7 @@ class Cart: pass self.session.modified = True - def build_usps_rate_request(self): + def build_usps_rate_request(self, container=None): return \ { 'service': ShippingService.PRIORITY_COMMERCIAL, @@ -160,7 +167,7 @@ class Cart: 'zip_destination': f'{self.session.get("shipping_address")["postal_code"]}', 'pounds': '0', 'ounces': f'{self.get_total_weight()}', - 'container': f'{self.get_shipping_box()}', + 'container': f'{self.get_shipping_box(container)}', 'width': '', 'length': '', 'height': '', @@ -168,7 +175,7 @@ class Cart: 'machinable': 'TRUE' } - def build_order_params(self): + def build_order_params(self, container=None): return \ { 'items': self, @@ -177,12 +184,14 @@ class Cart: 'discount': f'{self.get_discount()}', 'shipping_price': f'{self.get_shipping_cost()}', 'tax_total': '0', - 'shipping_method': 'US POSTAL SERVICE', + 'shipping_method': 'US POSTAL SERVICE ' + ( + container if container else '' + ), 'shipping_address': self.build_shipping_address(self.session.get('shipping_address')), } - def create_order(self): - params = self.build_order_params() + def create_order(self, container=None): + params = self.build_order_params(container) logger.info(f'\nParams: {params}\n') if settings.DEBUG: response = CreateOrder().create_order(params, debug=True) diff --git a/src/storefront/forms.py b/src/storefront/forms.py index 80f34e2..bf43d34 100644 --- a/src/storefront/forms.py +++ b/src/storefront/forms.py @@ -11,7 +11,7 @@ from usps import USPSApi, Address from captcha.fields import CaptchaField from core.models import Order -from core import CoffeeGrind +from core import CoffeeGrind, ShippingContainer from .tasks import contact_form_email @@ -106,6 +106,18 @@ class AddressForm(forms.Form): ) +class CheckoutShippingForm(forms.Form): + SHIPPING_CHOICES = [ + (ShippingContainer.MD_FLAT_RATE_BOX, 'Flate Rate Box - Medium'), + (ShippingContainer.REGIONAL_RATE_BOX_B, 'Regional Rate Box B'), + ] + + shipping_method = forms.ChoiceField( + widget=forms.RadioSelect, + choices=SHIPPING_CHOICES + ) + + class OrderCreateForm(forms.ModelForm): email = forms.CharField(widget=forms.HiddenInput()) first_name = forms.CharField(widget=forms.HiddenInput()) diff --git a/src/storefront/templates/storefront/checkout_address.html b/src/storefront/templates/storefront/checkout_address.html index 320df56..a618114 100644 --- a/src/storefront/templates/storefront/checkout_address.html +++ b/src/storefront/templates/storefront/checkout_address.html @@ -14,7 +14,7 @@ {% csrf_token %} {{form.as_p}}

- +

We validate addresses with USPS, if you are having issues please contact us at support@ptcoffee.com or use the contact information found on our contact page.

diff --git a/src/storefront/templates/storefront/checkout_shipping_form.html b/src/storefront/templates/storefront/checkout_shipping_form.html new file mode 100644 index 0000000..cd71ba3 --- /dev/null +++ b/src/storefront/templates/storefront/checkout_shipping_form.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% load static %} + +{% block head_title %}Checkout | {% endblock %} + +{% block content %} +
+
+

Checkout

+
+
+

Shipping Method

+
+ {% csrf_token %} + {{ form.non_field_errors }} +
+ {{ form.shipping_method.label }} + {% for radio in form.shipping_method %} +

+ + {{ radio.tag }} +

+ {% endfor %} +
+
+

+ +

+
+
+
+{% endblock %} diff --git a/src/storefront/urls.py b/src/storefront/urls.py index 313dca0..534df8b 100644 --- a/src/storefront/urls.py +++ b/src/storefront/urls.py @@ -48,6 +48,11 @@ urlpatterns = [ views.CheckoutAddressView.as_view(), name='checkout-address', ), + path( + 'checkout/shipping/', + views.CheckoutShippingView.as_view(), + name='checkout-shipping', + ), path('checkout/', views.OrderCreateView.as_view(), name='order-create'), path('done/', views.PaymentDoneView.as_view(), name='payment-done'), path( diff --git a/src/storefront/views.py b/src/storefront/views.py index d29436f..82c0e33 100644 --- a/src/storefront/views.py +++ b/src/storefront/views.py @@ -32,11 +32,11 @@ from accounts.forms import ( ) from core.models import Product, Order, Transaction, OrderLine, Coupon from core.forms import ShippingMethodForm -from core import OrderStatus +from core import OrderStatus, ShippingContainer from .forms import ( AddToCartForm, UpdateCartItemForm, OrderCreateForm, - AddressForm, CouponApplyForm, ContactForm + AddressForm, CouponApplyForm, ContactForm, CheckoutShippingForm, ) from .cart import Cart from .payments import CaptureOrder @@ -163,7 +163,7 @@ class ProductDetailView(FormMixin, DetailView): class CheckoutAddressView(FormView): template_name = 'storefront/checkout_address.html' form_class = AddressForm - success_url = reverse_lazy('storefront:order-create') + success_url = reverse_lazy('storefront:checkout-shipping') def get_initial(self): user = self.request.user @@ -212,6 +212,47 @@ class CheckoutAddressView(FormView): return super().form_valid(form) +class CheckoutShippingView(FormView): + template_name = 'storefront/checkout_shipping_form.html' + form_class = CheckoutShippingForm + success_url = reverse_lazy('storefront:order-create') + + def get(self, request, *args, **kwargs): + cart = Cart(request) + if len(cart) != 6: + if 'shipping_container' in self.request.session: + del self.request.session['shipping_container'] + return HttpResponseRedirect( + reverse('storefront:order-create') + ) + + if not self.request.session.get("shipping_address"): + messages.warning(request, 'Please add a shipping address.') + return HttpResponseRedirect( + reverse('storefront:checkout-address') + ) + + return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + cart = Cart(self.request) + context = super().get_context_data(**kwargs) + context['MD_FLAT_RATE_BOX'] = cart.get_shipping_cost( + ShippingContainer.MD_FLAT_RATE_BOX + ) + context['REGIONAL_RATE_BOX_B'] = cart.get_shipping_cost( + ShippingContainer.REGIONAL_RATE_BOX_B + ) + return context + + def form_valid(self, form): + cleaned_data = form.cleaned_data + self.request.session['shipping_container'] = cleaned_data.get( + 'shipping_method' + ) + return super().form_valid(form) + + class OrderCreateView(CreateView): model = Order template_name = 'storefront/order_form.html' @@ -219,6 +260,10 @@ class OrderCreateView(CreateView): success_url = reverse_lazy('storefront:payment-done') def get(self, request, *args, **kwargs): + cart = Cart(request) + if len(cart) != 6 and 'shipping_container' in self.request.session: + del self.request.session['shipping_container'] + if not self.request.session.get("shipping_address"): messages.warning(request, 'Please add a shipping address.') return HttpResponseRedirect( @@ -271,13 +316,14 @@ class OrderCreateView(CreateView): def form_valid(self, form): cart = Cart(self.request) shipping_address = self.request.session.get('shipping_address') + shipping_container = self.request.session.get('shipping_container') form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address) form.instance.status = OrderStatus.DRAFT self.object = form.save() bulk_list = cart.build_bulk_list(self.object) objs = OrderLine.objects.bulk_create(bulk_list) - response = cart.create_order() + response = cart.create_order(shipping_container) data = response.result.__dict__['_dict'] self.request.session['order_id'] = self.object.pk @@ -339,8 +385,8 @@ class CustomerDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView): permission_denied_message = 'Not authorized.' raise_exception = True - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) context['order_list'] = Order.objects.without_drafts().filter( customer=self.object ).prefetch_related('lines') From be3be2ac74a8b9e3ff18a7a833aa4cb0bcc13794 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Mon, 4 Jul 2022 11:59:58 -0600 Subject: [PATCH 2/3] Add blacklist to site --- .blacklist | 1 + src/storefront/tasks.py | 25 +++++++++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 .blacklist diff --git a/.blacklist b/.blacklist new file mode 100644 index 0000000..62d1fce --- /dev/null +++ b/.blacklist @@ -0,0 +1 @@ +mchretien@forum.dk diff --git a/src/storefront/tasks.py b/src/storefront/tasks.py index dadbf7f..ba0cdab 100644 --- a/src/storefront/tasks.py +++ b/src/storefront/tasks.py @@ -8,15 +8,24 @@ from templated_email import send_templated_mail logger = get_task_logger(__name__) -COTACT_FORM_TEMPLATE = 'storefront/contact_form' +CONTACT_FORM_TEMPLATE = 'storefront/contact_form' + @shared_task(name='contact_form_email') def contact_form_email(formdata): - send_templated_mail( - template_name=COTACT_FORM_TEMPLATE, - from_email=settings.DEFAULT_FROM_EMAIL, - recipient_list=[settings.DEFAULT_CONTACT_EMAIL], - context=formdata - ) + with open(f'{settings.BASE_DIR.parent}/.blacklist') as blacklist: + if formdata.get('email_address') not in blacklist.read(): + send_templated_mail( + template_name=CONTACT_FORM_TEMPLATE, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[settings.DEFAULT_CONTACT_EMAIL], + context=formdata + ) - logger.info(f"Contact form email sent from {formdata['email_address']}") + logger.info( + f"Contact form email sent from {formdata['email_address']}" + ) + else: + logger.warn( + f"{formdata['email_address']} tried to send an email but was on the blacklist" + ) From 25a174e0bcb72e79b66afaf89ce2bc494ba1020a Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Sat, 9 Jul 2022 08:13:09 -0600 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e95d46..a2243fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.16] - 2022-07-09 + +### Added +- Blacklist to reduce spam emails from contact form ## [1.3.14] - 2022-06-11