From e68a6080f261f2193c7e47466b939759c8d7d68f Mon Sep 17 00:00:00 2001
From: Nathan Chapman
Date: Sun, 8 May 2022 10:24:00 -0600
Subject: [PATCH 1/2] Better Logging
---
src/ptcoffee/settings.py | 75 ++++++++++++++++------------------------
1 file changed, 29 insertions(+), 46 deletions(-)
diff --git a/src/ptcoffee/settings.py b/src/ptcoffee/settings.py
index 793f7a5..a38a457 100644
--- a/src/ptcoffee/settings.py
+++ b/src/ptcoffee/settings.py
@@ -190,57 +190,40 @@ TEMPLATED_EMAIL_BACKEND = 'templated_email.backends.vanilla_django.TemplateBacke
SITE_ID = 1
# Logging
-if DEBUG:
- LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': False,
- 'formatters': {
- 'verbose': {
- 'format': '{name} {levelname} {asctime} {module} {process:d} {thread:d} {message}',
- 'style': '{',
- }
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'filters': {
+ 'require_debug_false': {
+ '()': 'django.utils.log.RequireDebugFalse',
},
- 'handlers': {
- 'console': {
- 'class': 'logging.StreamHandler',
- 'formatter': 'verbose',
- },
+ 'require_debug_true': {
+ '()': 'django.utils.log.RequireDebugTrue',
},
- 'root': {
- 'handlers': ['console'],
+ },
+ 'formatters': {
+ 'verbose': {
+ 'format': '{name} {levelname} {asctime} {module} {process:d} {thread:d} {message}',
+ 'style': '{',
+ },
+ },
+ 'handlers': {
+ 'file': {
'level': 'DEBUG',
+ 'class': 'logging.FileHandler',
+ 'filters': ['require_debug_false'],
+ 'filename': '/var/log/django-ptcoffee/debug.log',
+ 'formatter': 'verbose',
},
- }
-else:
- LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': False,
- 'formatters': {
- 'verbose': {
- 'format': '{name} {levelname} {asctime} {module} {process:d} {thread:d} {message}',
- 'style': '{',
- },
- 'simple': {
- 'format': '{levelname} {message}',
- 'style': '{',
- }
+ },
+ 'loggers': {
+ 'django.file': {
+ 'handlers': ['file'],
+ 'level': 'INFO',
+ 'propagate': True,
},
- 'handlers': {
- 'file': {
- 'level': 'DEBUG',
- 'class': 'logging.FileHandler',
- 'filename': '/var/log/django-ptcoffee/debug.log',
- 'formatter': 'verbose',
- },
- },
- 'loggers': {
- 'django': {
- 'handlers': ['file'],
- 'level': 'DEBUG',
- 'propagate': True,
- },
- },
- }
+ },
+}
CART_SESSION_ID = 'cart'
From 8587366e9e9e7bb47e1865193fd47831d6d5dc6c Mon Sep 17 00:00:00 2001
From: Nathan Chapman
Date: Sun, 8 May 2022 10:24:23 -0600
Subject: [PATCH 2/2] Add functional tests to test addresses
---
src/functional_tests/test_address.py | 60 ++++++++++
.../{tests_home.py => test_home.py} | 0
src/static/styles/main.css | 35 +++++-
src/storefront/cart.py | 9 +-
src/storefront/forms.py | 57 +++++++++-
.../storefront/checkout_address.html | 1 +
src/storefront/tests/test_forms.py | 106 ++++++++++++++++++
src/storefront/tests/test_views.py | 32 ++++++
src/storefront/views.py | 26 ++++-
9 files changed, 314 insertions(+), 12 deletions(-)
create mode 100644 src/functional_tests/test_address.py
rename src/functional_tests/{tests_home.py => test_home.py} (100%)
create mode 100644 src/storefront/tests/test_forms.py
create mode 100644 src/storefront/tests/test_views.py
diff --git a/src/functional_tests/test_address.py b/src/functional_tests/test_address.py
new file mode 100644
index 0000000..374d3ea
--- /dev/null
+++ b/src/functional_tests/test_address.py
@@ -0,0 +1,60 @@
+import os, time
+from selenium.webdriver.firefox.webdriver import WebDriver
+from selenium.webdriver.common.keys import Keys
+from selenium.webdriver.support.ui import Select
+from selenium.common.exceptions import WebDriverException
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+from django.contrib.staticfiles.testing import StaticLiveServerTestCase
+
+class AddressTests(StaticLiveServerTestCase):
+ fixtures = ['products.json']
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.browser = WebDriver()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.browser.quit()
+ super().tearDownClass()
+
+ def test_invalid_address_returns_errorlist(self):
+ self.browser.get(self.live_server_url + '/checkout/address/')
+ self.assertEqual(
+ self.browser.title,
+ 'Checkout | Port Townsend Roasting Co.'
+ )
+
+ full_name_input = self.browser.find_element_by_name("full_name")
+ full_name_input.send_keys('John Doe')
+ email_input = self.browser.find_element_by_id('id_email')
+ email_input.send_keys('john@example.com')
+ street_address_1_input = self.browser.find_element_by_name('street_address_1')
+ street_address_1_input.send_keys('1579')
+ city_input = self.browser.find_element_by_name('city')
+ city_input.send_keys('Logan')
+ state_select = select = Select(self.browser.find_element_by_name('state'))
+ 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()
+ # try:
+ # WebDriverWait(self.browser, 4).until(
+ # EC.presence_of_element_located((By.CLASS_NAME, 'errorlist'))
+ # )
+ # finally:
+ # self.browser.quit()
+
+ self.assertEqual(
+ self.browser.find_element_by_css_selector(
+ '.errorlist li'
+ ).text,
+ 'USPS: Address Not Found.'
+ )
+
+
+
+
diff --git a/src/functional_tests/tests_home.py b/src/functional_tests/test_home.py
similarity index 100%
rename from src/functional_tests/tests_home.py
rename to src/functional_tests/test_home.py
diff --git a/src/static/styles/main.css b/src/static/styles/main.css
index 745fd84..329a342 100644
--- a/src/static/styles/main.css
+++ b/src/static/styles/main.css
@@ -193,7 +193,7 @@ input[type=submit],
color: var(--fg-color);
background-color: var(--yellow-color);
- padding: 0.25rem 1rem;
+ padding: 0.4rem 1rem;
border-radius: 0.2rem;
border: none;
@@ -206,6 +206,15 @@ input[type=submit]:hover,
background-color: var(--yellow-alt-color);
}
+.errorlist {
+ background-color: var(--red-color);
+ color: white;
+ list-style: none;
+ padding: 0 1rem;
+ box-sizing: border-box;
+ font-weight: bold;
+}
+
/* Contact form
========================================================================== */
@@ -788,11 +797,33 @@ article + article {
/* Checkout / Shipping Address
========================================================================== */
+.checkout__address-form {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0 2rem;
+}
+
+
+.checkout__address-form .errorlist {
+ grid-column: span 2;
+}
+
+@media screen and (max-width: 600px) {
+ .checkout__address-form {
+ grid-template-columns: 1fr;
+ }
+ .checkout__address-form .errorlist {
+ grid-column: 1;
+}
+}
+.checkout__address-form p:last-child {
+ align-self: end;
+}
+
.checkout__address-form input,
.checkout__address-form select {
display: block;
width: 100%;
- max-width: 24rem;
}
.checkout__address {
diff --git a/src/storefront/cart.py b/src/storefront/cart.py
index c4596ff..5ab3a40 100644
--- a/src/storefront/cart.py
+++ b/src/storefront/cart.py
@@ -121,7 +121,14 @@ class Cart:
except TypeError as e:
return Decimal('0.00')
usps = USPSApiWithRate(settings.USPS_USER_ID, test=True)
- validation = usps.get_rate(usps_rate_request)
+
+ try:
+ validation = usps.get_rate(usps_rate_request)
+ except ConnectionError:
+ raise ValidationError(
+ 'Could not connect to USPS, try again.'
+ )
+
logger.info(validation.result)
if not 'Error' in validation.result['RateV4Response']['Package']:
rate = validation.result['RateV4Response']['Package']['Postage']['CommercialRate']
diff --git a/src/storefront/forms.py b/src/storefront/forms.py
index cfa559c..78891a5 100644
--- a/src/storefront/forms.py
+++ b/src/storefront/forms.py
@@ -1,6 +1,10 @@
-import logging
+import logging, json
+from requests import ConnectionError
from django import forms
+from django.conf import settings
from django.core.mail import EmailMessage
+from django.core.exceptions import ValidationError
+from usps import USPSApi, Address
from core.models import Order
from core import CoffeeGrind
@@ -35,8 +39,7 @@ class AddToSubscriptionForm(forms.Form):
class AddressForm(forms.Form):
- first_name = forms.CharField()
- last_name = forms.CharField()
+ full_name = forms.CharField()
email = forms.EmailField()
street_address_1 = forms.CharField()
street_address_2 = forms.CharField(required=False)
@@ -46,6 +49,54 @@ class AddressForm(forms.Form):
)
postal_code = forms.CharField()
+ def process_full_name(self, full_name):
+ name = full_name.split()
+
+ if len(name) > 2:
+ last_name = ''.join(name.pop(-1))
+ first_name = ' '.join(name)
+ elif len(name) > 1:
+ first_name = name[0]
+ last_name = name[1]
+ else:
+ first_name = name[0]
+ last_name = ''
+
+ return first_name, last_name
+
+ def clean(self):
+ cleaned_data = super().clean()
+ address = Address(
+ name=cleaned_data.get('full_name'),
+ address_1=cleaned_data.get('street_address_1'),
+ address_2=cleaned_data.get('street_address_2'),
+ city=cleaned_data.get('city'),
+ state=cleaned_data.get('state'),
+ zipcode=cleaned_data.get('postal_code')
+ )
+ usps = USPSApi(settings.USPS_USER_ID, test=True)
+
+ try:
+ validation = usps.validate_address(address)
+ except ConnectionError:
+ raise ValidationError(
+ 'Could not connect to USPS, try again.'
+ )
+
+
+ if 'Error' in validation.result['AddressValidateResponse']['Address']:
+ error = validation.result['AddressValidateResponse']['Address']['Error']['Description']
+ raise ValidationError(
+ "USPS: " + error
+ )
+
+ try:
+ cleaned_data['postal_code'] = validation.result['AddressValidateResponse']['Address']['Zip5']
+ except KeyError:
+ raise ValidationError(
+ 'Could not find Zip5'
+ )
+
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 29043ab..320df56 100644
--- a/src/storefront/templates/storefront/checkout_address.html
+++ b/src/storefront/templates/storefront/checkout_address.html
@@ -17,6 +17,7 @@
+ 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.
{% endblock %}
diff --git a/src/storefront/tests/test_forms.py b/src/storefront/tests/test_forms.py
new file mode 100644
index 0000000..60c1ff8
--- /dev/null
+++ b/src/storefront/tests/test_forms.py
@@ -0,0 +1,106 @@
+import logging
+from decimal import Decimal
+
+from measurement.measures import Weight
+from django.test import TestCase, Client, RequestFactory
+from django.urls import reverse
+from django.conf import settings
+from django.contrib.sessions.middleware import SessionMiddleware
+from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
+from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
+
+from accounts.models import User, Address
+from core.models import Product, Order
+from core import CoffeeGrind
+from storefront.views import OrderCreateView
+from storefront.forms import AddressForm
+from storefront.cart import Cart
+from storefront.payments import CreateOrder
+
+from . import RequestFaker
+
+logger = logging.getLogger(__name__)
+
+class AddressFormTest(TestCase):
+
+ def test_invalid_address_returns_form_error(self):
+ form = AddressForm(data={
+ 'full_name': 'John Doe',
+ 'email': 'john@example.com',
+ # Wrong street address
+ 'street_address_1': '1579 ',
+ 'street_address_2': '',
+ 'city': 'Logan',
+ 'state': 'UT',
+ # Wrong Zip code
+ 'postal_code': '23481'
+ })
+ self.assertFalse(form.is_valid())
+
+ def test_usps_finds_zip_from_address(self):
+ form = AddressForm(data={
+ 'full_name': 'John Doe',
+ 'email': 'john@example.com',
+ 'street_address_1': '1579 Talon Dr.',
+ 'street_address_2': '',
+ 'city': 'Logan',
+ 'state': 'UT',
+ # Wrong Zip code
+ 'postal_code': '23481'
+ })
+ self.assertTrue(form.is_valid())
+ if form.is_valid():
+ cleaned_data = form.cleaned_data
+ postal_code = cleaned_data.get('postal_code')
+ self.assertEqual(postal_code, '84321')
+
+
+ def test_invalid_address_returns_form_error(self):
+ form = AddressForm(data={
+ 'full_name': 'John Doe',
+ 'email': 'john@example.com',
+ # Wrong street address
+ 'street_address_1': '1579',
+ 'street_address_2': '',
+ 'city': 'Logan',
+ 'state': 'UT',
+ # Wrong Zip code
+ 'postal_code': '84321'
+ })
+ self.assertFalse(form.is_valid())
+
+ def test_process_full_name_with_two_given_names(self):
+ form = AddressForm(data={
+ 'full_name': 'John Doe',
+ 'email': 'john@example.com',
+ 'street_address_1': '1579 Talon Dr',
+ 'street_address_2': '',
+ 'city': 'Logan',
+ 'state': 'UT',
+ 'postal_code': '84321'
+ })
+ if form.is_valid():
+ cleaned_data = form.cleaned_data
+ first_name, last_name = form.process_full_name(
+ cleaned_data.get('full_name')
+ )
+ self.assertEqual(first_name, 'John')
+ self.assertEqual(last_name, 'Doe')
+
+ def test_process_full_name_with_more_than_two_given_names(self):
+ form = AddressForm(data={
+ 'full_name': 'John Franklin Rosevelt Doe',
+ 'email': 'john@example.com',
+ 'street_address_1': '1579 Talon Dr',
+ 'street_address_2': '',
+ 'city': 'Logan',
+ 'state': 'UT',
+ 'postal_code': '84321'
+ })
+ if form.is_valid():
+ cleaned_data = form.cleaned_data
+ first_name, last_name = form.process_full_name(
+ cleaned_data.get('full_name')
+ )
+ self.assertEqual(first_name, 'John Franklin Rosevelt')
+ self.assertEqual(last_name, 'Doe')
diff --git a/src/storefront/tests/test_views.py b/src/storefront/tests/test_views.py
new file mode 100644
index 0000000..6ab2011
--- /dev/null
+++ b/src/storefront/tests/test_views.py
@@ -0,0 +1,32 @@
+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
+
+from accounts.models import User, Address
+from core.models import Product, Order
+from core import CoffeeGrind
+from storefront.forms import AddressForm, OrderCreateForm
+from storefront.views import OrderCreateView, CheckoutAddressView
+from storefront.cart import Cart
+
+logger = logging.getLogger(__name__)
+
+class CheckoutAddressViewTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+
+ def test_view_uses_correct_template(self):
+ response = self.client.get(reverse('storefront:checkout-address'))
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'storefront/checkout_address.html')
+
+ def test_view_has_correct_form(self):
+ response = self.client.get(reverse('storefront:checkout-address'))
+ self.assertTrue(response.context['form'])
+ self.assertTrue(isinstance(response.context['form'], AddressForm))
diff --git a/src/storefront/views.py b/src/storefront/views.py
index a915cb5..a2a31cf 100644
--- a/src/storefront/views.py
+++ b/src/storefront/views.py
@@ -160,8 +160,7 @@ class CheckoutAddressView(FormView):
if user.is_authenticated and user.default_shipping_address:
address = user.default_shipping_address
initial = {
- 'first_name': address.first_name,
- 'last_name': address.last_name,
+ 'full_name': address.first_name+' '+address.last_name,
'email': user.email,
'street_address_1': address.street_address_1,
'street_address_2': address.street_address_2,
@@ -172,8 +171,7 @@ class CheckoutAddressView(FormView):
elif self.request.session.get('shipping_address'):
address = self.request.session.get('shipping_address')
initial = {
- 'first_name': address['first_name'],
- 'last_name': address['last_name'],
+ 'full_name': address['first_name']+' '+address['last_name'],
'email': address['email'],
'street_address_1': address['street_address_1'],
'street_address_2': address['street_address_2'],
@@ -185,7 +183,21 @@ class CheckoutAddressView(FormView):
def form_valid(self, form):
# save address data to session
- self.request.session['shipping_address'] = form.cleaned_data
+ cleaned_data = form.cleaned_data
+ first_name, last_name = form.process_full_name(
+ cleaned_data.get('full_name')
+ )
+ address = {
+ 'first_name': first_name,
+ 'last_name': last_name,
+ 'email': cleaned_data['email'],
+ 'street_address_1': cleaned_data['street_address_1'],
+ 'street_address_2': cleaned_data['street_address_2'],
+ 'city': cleaned_data['city'],
+ 'state': cleaned_data['state'],
+ 'postal_code': cleaned_data['postal_code']
+ }
+ self.request.session['shipping_address'] = address
return super().form_valid(form)
class OrderCreateView(CreateView):
@@ -197,7 +209,9 @@ class OrderCreateView(CreateView):
def get(self, request, *args, **kwargs):
if not self.request.session.get("shipping_address"):
messages.warning(request, 'Please add a shipping address.')
- return HttpResponseRedirect(reverse('storefront:checkout-address'))
+ return HttpResponseRedirect(
+ reverse('storefront:checkout-address')
+ )
else:
return super().get(request, *args, **kwargs)