Merge branch 'release/1.1.0'

This commit is contained in:
Nathan Chapman 2022-05-08 12:14:40 -06:00
commit b24b65da5c
14 changed files with 394 additions and 62 deletions

View File

@ -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.'
)

View File

@ -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'

View File

@ -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 {

View File

@ -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']

View File

@ -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())

View File

@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% block content %}
<article>
<header>
<p><a href="{% url 'storefront:customer-detail' user.pk %}">&larr; Back</a></p>
<h1>Create Address</h1>
</header>
<section>
<form method="post" action="{% url 'storefront:customer-address-create' customer.pk %}">
{% csrf_token %}
{{ form.as_p }}
<p>
<input type="submit" value="Create address">
</p>
</form>
</section>
</article>
{% endblock %}

View File

@ -17,6 +17,7 @@
<input type="submit" value="Continue to Payment">
</p>
</form>
<p>We validate addresses with USPS, if you are having issues please contact us at <a href="mailto:support@ptcoffee.com">support@ptcoffee.com</a> or use the contact information found on our <a href="{% url 'storefront:contact' %}">contact</a> page.</p>
</section>
</article>
{% endblock %}

View File

@ -30,8 +30,13 @@
</address>
{% endwith %}
</p>
</section>
<section>
<h4>Your addresses</h4>
<p>
<a href="{% url 'storefront:customer-address-create' user.pk %}" class="action-button">+ New address</a>
</p>
<div>
<p><strong>All addresses</strong></p>
{% for address in customer.addresses.all %}
<p>
<address>

View File

@ -15,6 +15,7 @@
</header>
<section class="checkout__address">
<h3>Shipping address</h3>
<p>{{shipping_address.email}}</p>
<address>
{{shipping_address.first_name}}
{{shipping_address.last_name}}<br>

View File

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

View File

@ -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))

View File

@ -33,7 +33,8 @@ urlpatterns = [
# path('delete/', views.CustomerDeleteView.as_view(), name='customer-delete'),
path('orders/<int:order_pk>/', views.OrderDetailView.as_view(), name='order-detail'),
path('addresses/<int:address_pk>/update/', views.AddressUpdateView.as_view(), name='address-update'),
path('addresses/new/', views.CustomerAddressCreateView.as_view(), name='customer-address-create'),
path('addresses/<int:address_pk>/update/', views.CustomerAddressUpdateView.as_view(), name='address-update'),
])),
]

View File

@ -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)
@ -295,6 +309,7 @@ class CustomerUpdateView(LoginRequiredMixin, UpdateView):
def get_success_url(self):
return reverse('storefront:customer-detail', kwargs={'pk': self.object.pk})
class OrderDetailView(LoginRequiredMixin, DetailView):
model = Order
template_name = 'storefront/order_detail.html'
@ -305,7 +320,27 @@ class OrderDetailView(LoginRequiredMixin, DetailView):
context['customer'] = User.objects.get(pk=self.kwargs['pk'])
return context
class AddressUpdateView(LoginRequiredMixin, UpdateView):
class CustomerAddressCreateView(LoginRequiredMixin, CreateView):
model = Address
template_name = 'storefront/address_create_form.html'
form_class = AccountAddressForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['customer'] = User.objects.get(pk=self.kwargs['pk'])
return context
def form_valid(self, form):
customer = User.objects.get(pk=self.kwargs['pk'])
self.object = form.save()
customer.addresses.add(self.object)
return super().form_valid(form)
def get_success_url(self):
return reverse('storefront:customer-detail', kwargs={'pk': self.kwargs['pk']})
class CustomerAddressUpdateView(LoginRequiredMixin, UpdateView):
model = Address
pk_url_kwarg = 'address_pk'
template_name = 'storefront/address_form.html'
@ -317,7 +352,7 @@ class AddressUpdateView(LoginRequiredMixin, UpdateView):
return context
def get_success_url(self):
return reverse('storefront:customer-detail',kwargs={'pk': self.kwargs['pk']})
return reverse('storefront:customer-detail', kwargs={'pk': self.kwargs['pk']})