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/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' 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)