diff --git a/src/accounts/fixtures/accounts.json b/src/accounts/fixtures/accounts.json
index fe40708..7087acb 100644
--- a/src/accounts/fixtures/accounts.json
+++ b/src/accounts/fixtures/accounts.json
@@ -1 +1,65 @@
-[{"model": "accounts.address", "pk": 1, "fields": {"first_name": "Nathan", "last_name": "Chapman", "street_address_1": "1504 N 230 E", "street_address_2": "", "city": "North Logan", "state": "UT", "postal_code": "84341"}}, {"model": "accounts.address", "pk": 2, "fields": {"first_name": "Nathan", "last_name": "Chapman", "street_address_1": "1125 W 400 N", "street_address_2": "", "city": "Logan", "state": "UT", "postal_code": "84321"}}, {"model": "accounts.user", "pk": 1, "fields": {"password": "pbkdf2_sha256$320000$VLksxVkUXOtoAEthHFi6DB$xj+81uh6NwPfZFP+7agf2UAJQZN89j5Wt38gUXeJ3z0=", "last_login": "2022-05-03T23:58:18.358Z", "is_superuser": true, "username": "nathanchapman", "first_name": "Nathan", "last_name": "Chapman", "email": "contact@nathanjchapman.com", "is_staff": true, "is_active": true, "date_joined": "2022-04-28T01:24:47.591Z", "default_shipping_address": 1, "default_billing_address": null, "groups": [], "user_permissions": [], "addresses": []}}, {"model": "accounts.user", "pk": 13, "fields": {"password": "pbkdf2_sha256$320000$L6WDkOMJwmkjR9OVsXfsIj$otr4goV5Tz5Hy5l24UkSYcH0L9Y5hDD89GKYD6LGcZo=", "last_login": null, "is_superuser": false, "username": "john", "first_name": "John", "last_name": "Doe", "email": "john@example.com", "is_staff": false, "is_active": true, "date_joined": "2022-05-04T00:00:11Z", "default_shipping_address": null, "default_billing_address": null, "groups": [], "user_permissions": [], "addresses": []}}]
\ No newline at end of file
+[{
+ "model": "accounts.address",
+ "pk": 1,
+ "fields": {
+ "first_name": "Nathan",
+ "last_name": "Chapman",
+ "street_address_1": "1504 N 230 E",
+ "street_address_2": "",
+ "city": "North Logan",
+ "state": "UT",
+ "postal_code": "84341"
+ }
+}, {
+ "model": "accounts.address",
+ "pk": 2,
+ "fields": {
+ "first_name": "John",
+ "last_name": "Doe",
+ "street_address_1": "90415 Pollich Skyway",
+ "street_address_2": "",
+ "city": "Jaskolskiburgh",
+ "state": "MS",
+ "postal_code": "32715"
+ }
+}, {
+ "model": "accounts.user",
+ "pk": 1,
+ "fields": {
+ "password": "pbkdf2_sha256$320000$VLksxVkUXOtoAEthHFi6DB$xj+81uh6NwPfZFP+7agf2UAJQZN89j5Wt38gUXeJ3z0=",
+ "last_login": "2022-05-03T23:58:18.358Z",
+ "is_superuser": true,
+ "username": "nathanchapman",
+ "first_name": "Nathan",
+ "last_name": "Chapman",
+ "email": "contact@nathanjchapman.com",
+ "is_staff": true,
+ "is_active": true,
+ "date_joined": "2022-04-28T01:24:47.591Z",
+ "default_shipping_address": 1,
+ "default_billing_address": null,
+ "groups": [],
+ "user_permissions": [],
+ "addresses": []
+ }
+}, {
+ "model": "accounts.user",
+ "pk": 2,
+ "fields": {
+ "password": "pbkdf2_sha256$320000$L6WDkOMJwmkjR9OVsXfsIj$otr4goV5Tz5Hy5l24UkSYcH0L9Y5hDD89GKYD6LGcZo=",
+ "last_login": null,
+ "is_superuser": false,
+ "username": "johndoe",
+ "first_name": "John",
+ "last_name": "Doe",
+ "email": "john@example.com",
+ "is_staff": false,
+ "is_active": true,
+ "date_joined": "2022-05-04T00:00:11Z",
+ "default_shipping_address": 2,
+ "default_billing_address": null,
+ "groups": [],
+ "user_permissions": [],
+ "addresses": []
+ }
+}]
diff --git a/src/core/fixtures/coupons.json b/src/core/fixtures/coupons.json
new file mode 100644
index 0000000..966274d
--- /dev/null
+++ b/src/core/fixtures/coupons.json
@@ -0,0 +1,29 @@
+[{
+ "model": "core.coupon",
+ "pk": 1,
+ "fields": {
+ "type": "entire_order",
+ "name": "Save 10%: Valid",
+ "code": "MAY2022",
+ "valid_from": "2022-05-01T06:00:00Z",
+ "valid_to": "2022-05-31T06:00:00Z",
+ "discount_value_type": "percentage",
+ "discount_value": "10.00",
+ "products": [],
+ "users": [1]
+ }
+}, {
+ "model": "core.coupon",
+ "pk": 2,
+ "fields": {
+ "type": "entire_order",
+ "name": "Save 10%: Invalid",
+ "code": "APR2022",
+ "valid_from": "2022-04-01T06:00:00Z",
+ "valid_to": "2022-04-30T06:00:00Z",
+ "discount_value_type": "percentage",
+ "discount_value": "10.00",
+ "products": [],
+ "users": []
+ }
+}]
diff --git a/src/core/migrations/0009_coupon_users.py b/src/core/migrations/0009_coupon_users.py
new file mode 100644
index 0000000..cb3293d
--- /dev/null
+++ b/src/core/migrations/0009_coupon_users.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.0.2 on 2022-05-11 00:55
+
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('core', '0008_alter_order_coupon_alter_order_weight'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='coupon',
+ name='users',
+ field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
+ ),
+ ]
diff --git a/src/core/models.py b/src/core/models.py
index fcbcf53..1c86e8c 100644
--- a/src/core/models.py
+++ b/src/core/models.py
@@ -27,11 +27,13 @@ from .weight import WeightUnits, zero_weight
logger = logging.getLogger(__name__)
+
class ProductEncoder(DjangoJSONEncoder):
def default(self, obj):
logger.info(f"\n{obj}\n")
return super().default(obj)
+
class ProductManager(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(
@@ -39,7 +41,6 @@ class ProductManager(models.Manager):
)
-
class Product(models.Model):
name = models.CharField(max_length=250)
subtitle = models.CharField(max_length=250, blank=True)
@@ -52,7 +53,10 @@ class Product(models.Model):
null=True,
)
weight = MeasurementField(
- measurement=Weight, unit_choices=WeightUnits.CHOICES, blank=True, null=True
+ measurement=Weight,
+ unit_choices=WeightUnits.CHOICES,
+ blank=True,
+ null=True
)
visible_in_listings = models.BooleanField(default=False)
@@ -105,11 +109,11 @@ class ProductPhoto(models.Model):
# img.save(self.image.path)
-
-
class Coupon(models.Model):
type = models.CharField(
- max_length=20, choices=VoucherType.CHOICES, default=VoucherType.ENTIRE_ORDER
+ max_length=20,
+ choices=VoucherType.CHOICES,
+ default=VoucherType.ENTIRE_ORDER
)
name = models.CharField(max_length=255, null=True, blank=True)
code = models.CharField(max_length=12, unique=True, db_index=True)
@@ -127,6 +131,7 @@ class Coupon(models.Model):
)
products = models.ManyToManyField(Product, blank=True)
+ users = models.ManyToManyField(User, blank=True)
class Meta:
ordering = ("code",)
@@ -143,8 +148,6 @@ class Coupon(models.Model):
return reverse('dashboard:coupon-detail', kwargs={'pk': self.pk})
-
-
class ShippingMethod(models.Model):
name = models.CharField(max_length=100)
type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES)
@@ -183,7 +186,9 @@ class Order(models.Model):
null=True
)
status = models.CharField(
- max_length=32, default=OrderStatus.UNFULFILLED, choices=OrderStatus.CHOICES
+ max_length=32,
+ default=OrderStatus.UNFULFILLED,
+ choices=OrderStatus.CHOICES
)
billing_address = models.ForeignKey(
Address,
@@ -207,7 +212,6 @@ class Order(models.Model):
on_delete=models.SET_NULL
)
-
coupon = models.ForeignKey(
Coupon,
related_name='orders',
@@ -228,7 +232,6 @@ class Order(models.Model):
default=0
)
-
weight = MeasurementField(
measurement=Weight,
unit_choices=WeightUnits.CHOICES,
@@ -263,7 +266,6 @@ class Order(models.Model):
ordering = ('-created_at',)
-
class Transaction(models.Model):
status = models.CharField(
max_length=32,
@@ -342,4 +344,3 @@ class TrackingNumber(models.Model):
def __str__(self):
return self.tracking_id
-
diff --git a/src/functional_tests/test_address.py b/src/functional_tests/test_address.py
index 374d3ea..2a85ca9 100644
--- a/src/functional_tests/test_address.py
+++ b/src/functional_tests/test_address.py
@@ -54,7 +54,3 @@ class AddressTests(StaticLiveServerTestCase):
).text,
'USPS: Address Not Found.'
)
-
-
-
-
diff --git a/src/functional_tests/test_coupon.py b/src/functional_tests/test_coupon.py
new file mode 100644
index 0000000..01ee41a
--- /dev/null
+++ b/src/functional_tests/test_coupon.py
@@ -0,0 +1,100 @@
+import os
+import time
+import logging
+
+from django.test import TestCase, Client
+from django.conf import settings
+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
+
+logger = logging.getLogger(__name__)
+
+
+class CouponTests(StaticLiveServerTestCase):
+ fixtures = ['products.json', 'accounts.json', 'coupons.json']
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.browser = WebDriver()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.browser.quit()
+ super().tearDownClass()
+
+ def login(self):
+ self.browser.get('%s%s' % (self.live_server_url, '/accounts/login/'))
+ username_input = self.browser.find_element_by_name("login")
+ username_input.send_keys('john@example.com')
+ password_input = self.browser.find_element_by_name("password")
+ password_input.send_keys('Bf25XBdP4vdt2X9L')
+ self.browser.find_element_by_xpath('//input[@value="Login"]').click()
+
+ def test_driver_has_session(self):
+ self.browser.get(self.live_server_url)
+ session_id = self.browser.get_cookie('sessionid')
+ self.assertTrue(session_id)
+
+ def test_apply_coupon_to_order(self):
+ # Add item to cart
+ self.browser.get(self.live_server_url + '/products/1/')
+ self.browser.find_element_by_xpath(
+ '//input[@value="Add to cart"]'
+ ).click()
+ self.assertEqual(
+ self.browser.find_element_by_class_name('cart__count').text,
+ '1'
+ )
+
+ # Add coupon code
+ coupon_input = self.browser.find_element_by_id('id_code')
+ coupon_input.send_keys('MAY2022')
+ self.browser.find_element_by_xpath('//input[@value="Apply"]').click()
+ self.browser.find_element_by_xpath(
+ '//a[contains(text(), "Proceed to Checkout")]'
+ ).click()
+
+ # Add 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('contact@nathanjchapman.com')
+ street_address_1_input = self.browser.find_element_by_name(
+ 'street_address_1'
+ )
+ street_address_1_input.send_keys('1579 Talon Dr')
+ 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('84321')
+ self.browser.find_element_by_xpath(
+ '//input[@value="Continue to Payment"]'
+ ).click()
+
+ self.assertEqual(
+ self.browser.title,
+ 'Checkout | Port Townsend Roasting Co.'
+ )
+
+ message_text = self.browser.find_element_by_css_selector(
+ '.messages p'
+ ).text
+ self.assertEqual(
+ 'Coupon already used.',
+ message_text
+ )
diff --git a/src/functional_tests/test_home.py b/src/functional_tests/test_home.py
index 12ca962..122b194 100644
--- a/src/functional_tests/test_home.py
+++ b/src/functional_tests/test_home.py
@@ -1,9 +1,11 @@
-import os, time
+import os
+import time
from selenium.webdriver.firefox.webdriver import WebDriver
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import WebDriverException
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
+
class HomeTests(StaticLiveServerTestCase):
fixtures = ['accounts.json', 'products.json']
diff --git a/src/static/styles/main.css b/src/static/styles/main.css
index 5c98062..dd76c08 100644
--- a/src/static/styles/main.css
+++ b/src/static/styles/main.css
@@ -573,6 +573,11 @@ article + article {
margin-top: 8rem;
}
+.error-view {
+ text-align: center;
+ margin: auto 0;
+}
+
/* Product reviews
========================================================================== */
.review__list {
diff --git a/src/storefront/cart.py b/src/storefront/cart.py
index 4572634..c14db29 100644
--- a/src/storefront/cart.py
+++ b/src/storefront/cart.py
@@ -24,6 +24,7 @@ from .payments import CreateOrder
logger = logging.getLogger(__name__)
+
class Cart:
def __init__(self, request):
self.request = request
@@ -34,7 +35,9 @@ class Cart:
cart = self.session[settings.CART_SESSION_ID] = {}
self.cart = cart
- def add(self, request, product, quantity=1, grind='', update_quantity=False):
+ def add(
+ self, request, product, quantity=1, grind='', update_quantity=False
+ ):
product_id = str(product.id)
if product_id not in self.cart:
self.cart[product_id] = {
diff --git a/src/storefront/forms.py b/src/storefront/forms.py
index 51b9ff6..d1623ab 100644
--- a/src/storefront/forms.py
+++ b/src/storefront/forms.py
@@ -1,4 +1,5 @@
-import logging, json
+import logging
+import json
from requests import ConnectionError
from django import forms
from django.conf import settings
@@ -14,13 +15,19 @@ from .tasks import contact_form_email
logger = logging.getLogger(__name__)
+
class AddToCartForm(forms.Form):
grind = forms.ChoiceField(choices=CoffeeGrind.GRIND_CHOICES)
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
+
class UpdateCartItemForm(forms.Form):
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
- update = forms.BooleanField(required=False, initial=True, widget=forms.HiddenInput)
+ update = forms.BooleanField(
+ required=False,
+ initial=True,
+ widget=forms.HiddenInput
+ )
class AddToSubscriptionForm(forms.Form):
@@ -83,7 +90,6 @@ class AddressForm(forms.Form):
'Could not connect to USPS, try again.'
)
-
if 'Error' in validation.result['AddressValidateResponse']['Address']:
error = validation.result['AddressValidateResponse']['Address']['Error']['Description']
raise ValidationError(
@@ -97,6 +103,7 @@ class AddressForm(forms.Form):
'Could not find Zip5'
)
+
class OrderCreateForm(forms.ModelForm):
email = forms.CharField(widget=forms.HiddenInput())
first_name = forms.CharField(widget=forms.HiddenInput())
@@ -113,9 +120,11 @@ class OrderCreateForm(forms.ModelForm):
'shipping_total': forms.HiddenInput()
}
+
class CouponApplyForm(forms.Form):
code = forms.CharField(label='Coupon code')
+
class ContactForm(forms.Form):
GOOGLE = 'Google Search'
SHOP = 'The coffee shop'
diff --git a/src/storefront/tests/test_cart.py b/src/storefront/tests/test_cart.py
index af47581..5ea93de 100644
--- a/src/storefront/tests/test_cart.py
+++ b/src/storefront/tests/test_cart.py
@@ -16,15 +16,16 @@ from storefront.cart import Cart
logger = logging.getLogger(__name__)
-class CartTest(TestCase):
- def setUp(self):
- self.client = Client()
- self.factory = RequestFactory()
- self.customer = User.objects.create_user(
- username='petertempler', email='peter@testing.com', password='peterspassword321'
+class CartTest(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.customer = User.objects.create_user(
+ username='petertempler',
+ email='peter@testing.com',
+ password='peterspassword321'
)
- self.product = Product.objects.create(
+ cls.product = Product.objects.create(
name='Dante\'s Tornado',
description='Coffee',
sku='23987',
@@ -32,11 +33,15 @@ class CartTest(TestCase):
weight=Weight(oz=16),
visible_in_listings=True
)
- self.order = Order.objects.create(
- customer=self.customer,
+ cls.order = Order.objects.create(
+ customer=cls.customer,
total_net_amount=13.4
)
+ def setUp(self):
+ self.client = Client()
+ self.factory = RequestFactory()
+
self.client.force_login(self.customer)
self.client.session['shipping_address'] = {
'first_name': 'Nathan',
@@ -56,7 +61,6 @@ class CartTest(TestCase):
request = response.wsgi_request
cart = Cart(request)
- cart = Cart(request)
cart.add(
request,
product=self.product,
@@ -89,7 +93,10 @@ class CartTest(TestCase):
update_quantity=False
)
- self.assertEqual(cart.cart[f'{self.product.id}']['variations'][CoffeeGrind.WHOLE]['quantity'], 1)
+ self.assertEqual(
+ cart.cart[f'{self.product.id}']['variations'][CoffeeGrind.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'))
@@ -100,7 +107,10 @@ class CartTest(TestCase):
grind=CoffeeGrind.WHOLE,
update_quantity=False
)
- self.assertEqual(cart.cart[f'{self.product.id}']['variations'][CoffeeGrind.WHOLE]['quantity'], 2)
+ self.assertEqual(
+ cart.cart[f'{self.product.id}']['variations'][CoffeeGrind.WHOLE]['quantity'],
+ 2
+ )
self.assertEqual(len(cart), 2)
cart.add(
@@ -110,7 +120,10 @@ class CartTest(TestCase):
grind=CoffeeGrind.ESPRESSO,
update_quantity=False
)
- self.assertEqual(cart.cart[f'{self.product.id}']['variations'][CoffeeGrind.ESPRESSO]['quantity'], 3)
+ self.assertEqual(
+ cart.cart[f'{self.product.id}']['variations'][CoffeeGrind.ESPRESSO]['quantity'],
+ 3
+ )
self.assertEqual(len(cart), 5)
self.assertEqual(cart.get_total_price(), Decimal('67'))
@@ -128,7 +141,10 @@ class CartTest(TestCase):
grind=CoffeeGrind.WHOLE,
update_quantity=False
)
- self.assertEqual(cart.cart[f'{self.product.id}']['variations'][CoffeeGrind.WHOLE]['quantity'], 3)
+ self.assertEqual(
+ cart.cart[f'{self.product.id}']['variations'][CoffeeGrind.WHOLE]['quantity'],
+ 3
+ )
cart.add(
request,
@@ -137,7 +153,10 @@ class CartTest(TestCase):
grind=CoffeeGrind.WHOLE,
update_quantity=True
)
- self.assertEqual(cart.cart[f'{self.product.id}']['variations'][CoffeeGrind.WHOLE]['quantity'], 1)
+ self.assertEqual(
+ cart.cart[f'{self.product.id}']['variations'][CoffeeGrind.WHOLE]['quantity'],
+ 1
+ )
def test_cart_remove_item(self):
cart_detail_url = reverse('storefront:cart-detail')
@@ -172,6 +191,3 @@ class CartTest(TestCase):
update_quantity=False
)
self.assertEqual(cart.get_total_weight(), Decimal(48))
-
- def test_cart_(self):
- pass
diff --git a/src/storefront/tests/test_models.py b/src/storefront/tests/test_models.py
new file mode 100644
index 0000000..a90f008
--- /dev/null
+++ b/src/storefront/tests/test_models.py
@@ -0,0 +1,18 @@
+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__)
diff --git a/src/storefront/tests/test_payments.py b/src/storefront/tests/test_payments.py
index 8099658..1f4d059 100644
--- a/src/storefront/tests/test_payments.py
+++ b/src/storefront/tests/test_payments.py
@@ -21,9 +21,9 @@ from . import RequestFaker
logger = logging.getLogger(__name__)
class CreateOrderTest(TestCase):
- def setUp(self):
- self.client = Client()
- self.product = Product.objects.create(
+ @classmethod
+ def setUpTestData(cls):
+ cls.product = Product.objects.create(
name='Decaf',
description='Coffee',
sku='23987',
@@ -32,6 +32,10 @@ class CreateOrderTest(TestCase):
visible_in_listings=True
)
+ def setUp(self):
+ self.client = Client()
+
+
def test_build_request_body(self):
product_list_url = reverse('storefront:product-list')
response = self.client.get(product_list_url, follow=True)
diff --git a/src/storefront/tests/test_views.py b/src/storefront/tests/test_views.py
index 6ab2011..14e5755 100644
--- a/src/storefront/tests/test_views.py
+++ b/src/storefront/tests/test_views.py
@@ -9,7 +9,7 @@ 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.models import Product, Order, Coupon
from core import CoffeeGrind
from storefront.forms import AddressForm, OrderCreateForm
from storefront.views import OrderCreateView, CheckoutAddressView
@@ -17,7 +17,8 @@ from storefront.cart import Cart
logger = logging.getLogger(__name__)
-class CheckoutAddressViewTest(TestCase):
+
+class CheckoutAddressViewTests(TestCase):
def setUp(self):
self.client = Client()
@@ -30,3 +31,48 @@ class CheckoutAddressViewTest(TestCase):
response = self.client.get(reverse('storefront:checkout-address'))
self.assertTrue(response.context['form'])
self.assertTrue(isinstance(response.context['form'], AddressForm))
+
+
+class OrderCreateViewTests(TestCase):
+ fixtures = ['accounts.json', 'coupons.json']
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.customer = User.objects.get(pk=1)
+ cls.product = Product.objects.create(
+ name="Dante's Tornado",
+ description='Coffee',
+ sku='23987',
+ price=13.4,
+ weight=Weight(oz=16),
+ visible_in_listings=True
+ )
+ cls.order = Order.objects.create(
+ customer=cls.customer,
+ total_net_amount=13.4
+ )
+
+ def setUp(self):
+ self.client = Client()
+
+ def test_used_coupon_creates_error_on_checkout(self):
+ session = self.client.session
+ session['shipping_address'] = {
+ 'first_name': 'Nathan',
+ 'last_name': 'Chapman',
+ 'email': 'contact@nathanjchapman.com',
+ 'street_address_1': '1504 N 230 E',
+ 'street_address_2': '',
+ 'city': 'North Logan',
+ 'state': 'UT',
+ 'postal_code': '84341'
+ }
+ session['coupon_code'] = 'MAY2022'
+ session.save()
+
+ response = self.client.get(
+ reverse('storefront:order-create'), follow=True
+ )
+ self.assertTrue(self.client.session.get('shipping_address'))
+ self.assertTemplateUsed(response, 'storefront/order_form.html')
+ self.assertContains(response, 'Coupon already used', status_code=200)
diff --git a/src/storefront/views.py b/src/storefront/views.py
index 5adae4b..63da3e6 100644
--- a/src/storefront/views.py
+++ b/src/storefront/views.py
@@ -6,10 +6,12 @@ from django.utils import timezone
from django.shortcuts import render, reverse, redirect, get_object_or_404
from django.urls import reverse_lazy
from django.core.mail import EmailMessage
-from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.http import JsonResponse, HttpResponseRedirect
from django.views.generic.base import RedirectView, TemplateView
-from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView, FormMixin
+from django.views.generic.edit import (
+ FormView, CreateView, UpdateView, DeleteView, FormMixin
+)
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.list import ListView
from django.contrib.auth.decorators import login_required
@@ -25,17 +27,23 @@ from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
from accounts.models import User, Address
from accounts.utils import get_or_create_customer
-from accounts.forms import AddressForm as AccountAddressForm, CustomerUpdateForm
+from accounts.forms import (
+ AddressForm as AccountAddressForm, CustomerUpdateForm
+)
from core.models import Product, Order, Transaction, OrderLine, Coupon
from core.forms import ShippingMethodForm
from core import OrderStatus
-from .forms import AddToCartForm, UpdateCartItemForm, OrderCreateForm, AddressForm, CouponApplyForm, ContactForm
+from .forms import (
+ AddToCartForm, UpdateCartItemForm, OrderCreateForm,
+ AddressForm, CouponApplyForm, ContactForm
+)
from .cart import Cart
from .payments import CaptureOrder
logger = logging.getLogger(__name__)
+
class CartView(TemplateView):
template_name = 'storefront/cart_detail.html'
@@ -53,6 +61,7 @@ class CartView(TemplateView):
context['coupon_apply_form'] = CouponApplyForm()
return context
+
class CartAddProductView(SingleObjectMixin, FormView):
model = Product
form_class = AddToCartForm
@@ -118,7 +127,7 @@ class CouponApplyView(FormView):
def form_valid(self, form):
today = timezone.localtime(timezone.now()).date()
- code = form.cleaned_data['code']
+ code = form.cleaned_data['code'].upper()
try:
coupon = Coupon.objects.get(
code__iexact=code,
@@ -200,6 +209,7 @@ class CheckoutAddressView(FormView):
self.request.session['shipping_address'] = address
return super().form_valid(form)
+
class OrderCreateView(CreateView):
model = Order
template_name = 'storefront/order_form.html'
@@ -212,8 +222,17 @@ class OrderCreateView(CreateView):
return HttpResponseRedirect(
reverse('storefront:checkout-address')
)
- else:
- return super().get(request, *args, **kwargs)
+ elif self.request.session.get('coupon_code'):
+ address = self.request.session.get("shipping_address")
+ coupon = Coupon.objects.get(
+ code=self.request.session.get('coupon_code')
+ )
+ user = get_object_or_404(User, email=address['email'])
+ if user in coupon.users.all():
+ del self.request.session['coupon_code']
+ messages.warning(request, 'Coupon already used.')
+
+ return super().get(request, *args, **kwargs)
def get_initial(self):
cart = Cart(self.request)
@@ -249,6 +268,13 @@ class OrderCreateView(CreateView):
shipping_address = self.request.session.get('shipping_address')
form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address)
form.instance.status = OrderStatus.DRAFT
+ coupon = get_object_or_404(
+ Coupon,
+ code=self.request.session.get('coupon_code')
+ )
+ if coupon:
+ form.instance.coupon = coupon
+ coupon.users.add(form.instance.customer)
self.object = form.save()
bulk_list = cart.build_bulk_list(self.object)
objs = OrderLine.objects.bulk_create(bulk_list)
@@ -260,10 +286,11 @@ class OrderCreateView(CreateView):
return JsonResponse(data)
+
@csrf_exempt
@require_POST
def paypal_order_transaction_capture(request, transaction_id):
- if request.method =="POST":
+ if request.method == "POST":
data = CaptureOrder().capture_order(transaction_id)
cart = Cart(request)
cart.clear()
@@ -280,6 +307,7 @@ def paypal_order_transaction_capture(request, transaction_id):
else:
return JsonResponse({'details': 'invalid request'})
+
@csrf_exempt
@require_POST
def paypal_webhook_endpoint(request):
@@ -291,39 +319,66 @@ def paypal_webhook_endpoint(request):
class PaymentDoneView(TemplateView):
template_name = 'storefront/payment_done.html'
+
class PaymentCanceledView(TemplateView):
template_name = 'storefront/payment_canceled.html'
-class CustomerDetailView(LoginRequiredMixin, DetailView):
+class CustomerDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView):
model = User
template_name = 'storefront/customer_detail.html'
context_object_name = 'customer'
+ permission_denied_message = 'Not authorized.'
+ raise_exception = True
-class CustomerUpdateView(LoginRequiredMixin, UpdateView):
+ def test_func(self):
+ return self.request.user.pk == self.get_object().pk
+
+
+class CustomerUpdateView(UserPassesTestMixin, LoginRequiredMixin, UpdateView):
model = User
template_name = 'storefront/customer_form.html'
context_object_name = 'customer'
form_class = CustomerUpdateForm
+ permission_denied_message = 'Not authorized.'
+ raise_exception = True
+
+ def test_func(self):
+ return self.request.user.pk == self.get_object().pk
def get_success_url(self):
- return reverse('storefront:customer-detail', kwargs={'pk': self.object.pk})
+ return reverse(
+ 'storefront:customer-detail', kwargs={'pk': self.object.pk}
+ )
-class OrderDetailView(LoginRequiredMixin, DetailView):
+class OrderDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView):
model = Order
template_name = 'storefront/order_detail.html'
pk_url_kwarg = 'order_pk'
+ permission_denied_message = 'Not authorized.'
+ raise_exception = True
+
+ def test_func(self):
+ return self.request.user.pk == self.get_object().customer.pk
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['customer'] = User.objects.get(pk=self.kwargs['pk'])
return context
-class CustomerAddressCreateView(LoginRequiredMixin, CreateView):
+
+class CustomerAddressCreateView(
+ UserPassesTestMixin, LoginRequiredMixin, CreateView
+):
model = Address
template_name = 'storefront/address_create_form.html'
form_class = AccountAddressForm
+ permission_denied_message = 'Not authorized.'
+ raise_exception = True
+
+ def test_func(self):
+ return self.request.user.pk == self.kwargs['pk']
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -338,13 +393,23 @@ class CustomerAddressCreateView(LoginRequiredMixin, CreateView):
return super().form_valid(form)
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']}
+ )
-class CustomerAddressUpdateView(LoginRequiredMixin, UpdateView):
+
+class CustomerAddressUpdateView(
+ UserPassesTestMixin, LoginRequiredMixin, UpdateView
+):
model = Address
pk_url_kwarg = 'address_pk'
template_name = 'storefront/address_form.html'
form_class = AccountAddressForm
+ permission_denied_message = 'Not authorized.'
+ raise_exception = True
+
+ def test_func(self):
+ return self.request.user.pk == self.get_object().customer.pk
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -355,16 +420,18 @@ class CustomerAddressUpdateView(LoginRequiredMixin, UpdateView):
return reverse('storefront:customer-detail', kwargs={'pk': self.kwargs['pk']})
-
class AboutView(TemplateView):
template_name = 'storefront/about.html'
+
class FairTradeView(TemplateView):
template_name = 'storefront/fairtrade.html'
+
class ReviewListView(TemplateView):
template_name = 'storefront/reviews.html'
+
class ContactFormView(FormView, SuccessMessageMixin):
template_name = 'storefront/contact_form.html'
form_class = ContactForm
diff --git a/src/templates/400.html b/src/templates/400.html
new file mode 100644
index 0000000..9d4d467
--- /dev/null
+++ b/src/templates/400.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+ 400 Bad request
+
+{% endblock %}
diff --git a/src/templates/403.html b/src/templates/403.html
new file mode 100644
index 0000000..ed2d71d
--- /dev/null
+++ b/src/templates/403.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+ 403 Forbidden
+
+{% endblock %}
diff --git a/src/templates/404.html b/src/templates/404.html
new file mode 100644
index 0000000..fb0f071
--- /dev/null
+++ b/src/templates/404.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+ 404 Page not found
+
+{% endblock %}
diff --git a/src/templates/500.html b/src/templates/500.html
new file mode 100644
index 0000000..5c89c31
--- /dev/null
+++ b/src/templates/500.html
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+ 500 Server error
+
+{% endblock %}