Merge branch 'feature/user-permissions' into develop

This commit is contained in:
Nathan Chapman 2022-05-11 20:09:52 -06:00
commit 37ef9d3928
19 changed files with 469 additions and 61 deletions

View File

@ -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": []}}] [{
"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": []
}
}]

View File

@ -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": []
}
}]

View File

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

View File

@ -27,11 +27,13 @@ from .weight import WeightUnits, zero_weight
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class ProductEncoder(DjangoJSONEncoder): class ProductEncoder(DjangoJSONEncoder):
def default(self, obj): def default(self, obj):
logger.info(f"\n{obj}\n") logger.info(f"\n{obj}\n")
return super().default(obj) return super().default(obj)
class ProductManager(models.Manager): class ProductManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return super().get_queryset().annotate( return super().get_queryset().annotate(
@ -39,7 +41,6 @@ class ProductManager(models.Manager):
) )
class Product(models.Model): class Product(models.Model):
name = models.CharField(max_length=250) name = models.CharField(max_length=250)
subtitle = models.CharField(max_length=250, blank=True) subtitle = models.CharField(max_length=250, blank=True)
@ -52,7 +53,10 @@ class Product(models.Model):
null=True, null=True,
) )
weight = MeasurementField( 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) visible_in_listings = models.BooleanField(default=False)
@ -105,11 +109,11 @@ class ProductPhoto(models.Model):
# img.save(self.image.path) # img.save(self.image.path)
class Coupon(models.Model): class Coupon(models.Model):
type = models.CharField( 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) name = models.CharField(max_length=255, null=True, blank=True)
code = models.CharField(max_length=12, unique=True, db_index=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) products = models.ManyToManyField(Product, blank=True)
users = models.ManyToManyField(User, blank=True)
class Meta: class Meta:
ordering = ("code",) ordering = ("code",)
@ -143,8 +148,6 @@ class Coupon(models.Model):
return reverse('dashboard:coupon-detail', kwargs={'pk': self.pk}) return reverse('dashboard:coupon-detail', kwargs={'pk': self.pk})
class ShippingMethod(models.Model): class ShippingMethod(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES) type = models.CharField(max_length=30, choices=ShippingMethodType.CHOICES)
@ -183,7 +186,9 @@ class Order(models.Model):
null=True null=True
) )
status = models.CharField( 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( billing_address = models.ForeignKey(
Address, Address,
@ -207,7 +212,6 @@ class Order(models.Model):
on_delete=models.SET_NULL on_delete=models.SET_NULL
) )
coupon = models.ForeignKey( coupon = models.ForeignKey(
Coupon, Coupon,
related_name='orders', related_name='orders',
@ -228,7 +232,6 @@ class Order(models.Model):
default=0 default=0
) )
weight = MeasurementField( weight = MeasurementField(
measurement=Weight, measurement=Weight,
unit_choices=WeightUnits.CHOICES, unit_choices=WeightUnits.CHOICES,
@ -263,7 +266,6 @@ class Order(models.Model):
ordering = ('-created_at',) ordering = ('-created_at',)
class Transaction(models.Model): class Transaction(models.Model):
status = models.CharField( status = models.CharField(
max_length=32, max_length=32,
@ -342,4 +344,3 @@ class TrackingNumber(models.Model):
def __str__(self): def __str__(self):
return self.tracking_id return self.tracking_id

View File

@ -54,7 +54,3 @@ class AddressTests(StaticLiveServerTestCase):
).text, ).text,
'USPS: Address Not Found.' 'USPS: Address Not Found.'
) )

View File

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

View File

@ -1,9 +1,11 @@
import os, time import os
import time
from selenium.webdriver.firefox.webdriver import WebDriver from selenium.webdriver.firefox.webdriver import WebDriver
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import WebDriverException from selenium.common.exceptions import WebDriverException
from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.contrib.staticfiles.testing import StaticLiveServerTestCase
class HomeTests(StaticLiveServerTestCase): class HomeTests(StaticLiveServerTestCase):
fixtures = ['accounts.json', 'products.json'] fixtures = ['accounts.json', 'products.json']

View File

@ -573,6 +573,11 @@ article + article {
margin-top: 8rem; margin-top: 8rem;
} }
.error-view {
text-align: center;
margin: auto 0;
}
/* Product reviews /* Product reviews
========================================================================== */ ========================================================================== */
.review__list { .review__list {

View File

@ -24,6 +24,7 @@ from .payments import CreateOrder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Cart: class Cart:
def __init__(self, request): def __init__(self, request):
self.request = request self.request = request
@ -34,7 +35,9 @@ class Cart:
cart = self.session[settings.CART_SESSION_ID] = {} cart = self.session[settings.CART_SESSION_ID] = {}
self.cart = cart 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) product_id = str(product.id)
if product_id not in self.cart: if product_id not in self.cart:
self.cart[product_id] = { self.cart[product_id] = {

View File

@ -1,4 +1,5 @@
import logging, json import logging
import json
from requests import ConnectionError from requests import ConnectionError
from django import forms from django import forms
from django.conf import settings from django.conf import settings
@ -14,13 +15,19 @@ from .tasks import contact_form_email
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class AddToCartForm(forms.Form): class AddToCartForm(forms.Form):
grind = forms.ChoiceField(choices=CoffeeGrind.GRIND_CHOICES) grind = forms.ChoiceField(choices=CoffeeGrind.GRIND_CHOICES)
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1) quantity = forms.IntegerField(min_value=1, max_value=20, initial=1)
class UpdateCartItemForm(forms.Form): class UpdateCartItemForm(forms.Form):
quantity = forms.IntegerField(min_value=1, max_value=20, initial=1) 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): class AddToSubscriptionForm(forms.Form):
@ -83,7 +90,6 @@ class AddressForm(forms.Form):
'Could not connect to USPS, try again.' 'Could not connect to USPS, try again.'
) )
if 'Error' in validation.result['AddressValidateResponse']['Address']: if 'Error' in validation.result['AddressValidateResponse']['Address']:
error = validation.result['AddressValidateResponse']['Address']['Error']['Description'] error = validation.result['AddressValidateResponse']['Address']['Error']['Description']
raise ValidationError( raise ValidationError(
@ -97,6 +103,7 @@ class AddressForm(forms.Form):
'Could not find Zip5' 'Could not find Zip5'
) )
class OrderCreateForm(forms.ModelForm): class OrderCreateForm(forms.ModelForm):
email = forms.CharField(widget=forms.HiddenInput()) email = forms.CharField(widget=forms.HiddenInput())
first_name = forms.CharField(widget=forms.HiddenInput()) first_name = forms.CharField(widget=forms.HiddenInput())
@ -113,9 +120,11 @@ class OrderCreateForm(forms.ModelForm):
'shipping_total': forms.HiddenInput() 'shipping_total': forms.HiddenInput()
} }
class CouponApplyForm(forms.Form): class CouponApplyForm(forms.Form):
code = forms.CharField(label='Coupon code') code = forms.CharField(label='Coupon code')
class ContactForm(forms.Form): class ContactForm(forms.Form):
GOOGLE = 'Google Search' GOOGLE = 'Google Search'
SHOP = 'The coffee shop' SHOP = 'The coffee shop'

View File

@ -16,15 +16,16 @@ from storefront.cart import Cart
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CartTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.customer = User.objects.create_user( class CartTest(TestCase):
username='petertempler', email='peter@testing.com', password='peterspassword321' @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', name='Dante\'s Tornado',
description='Coffee', description='Coffee',
sku='23987', sku='23987',
@ -32,11 +33,15 @@ class CartTest(TestCase):
weight=Weight(oz=16), weight=Weight(oz=16),
visible_in_listings=True visible_in_listings=True
) )
self.order = Order.objects.create( cls.order = Order.objects.create(
customer=self.customer, customer=cls.customer,
total_net_amount=13.4 total_net_amount=13.4
) )
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.client.force_login(self.customer) self.client.force_login(self.customer)
self.client.session['shipping_address'] = { self.client.session['shipping_address'] = {
'first_name': 'Nathan', 'first_name': 'Nathan',
@ -56,7 +61,6 @@ class CartTest(TestCase):
request = response.wsgi_request request = response.wsgi_request
cart = Cart(request) cart = Cart(request)
cart = Cart(request)
cart.add( cart.add(
request, request,
product=self.product, product=self.product,
@ -89,7 +93,10 @@ class CartTest(TestCase):
update_quantity=False 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(len(cart), 1)
self.assertEqual(sum(cart.get_item_prices()), Decimal('13.4')) self.assertEqual(sum(cart.get_item_prices()), Decimal('13.4'))
self.assertEqual(cart.get_total_price(), Decimal('13.4')) self.assertEqual(cart.get_total_price(), Decimal('13.4'))
@ -100,7 +107,10 @@ class CartTest(TestCase):
grind=CoffeeGrind.WHOLE, grind=CoffeeGrind.WHOLE,
update_quantity=False 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) self.assertEqual(len(cart), 2)
cart.add( cart.add(
@ -110,7 +120,10 @@ class CartTest(TestCase):
grind=CoffeeGrind.ESPRESSO, grind=CoffeeGrind.ESPRESSO,
update_quantity=False 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(len(cart), 5)
self.assertEqual(cart.get_total_price(), Decimal('67')) self.assertEqual(cart.get_total_price(), Decimal('67'))
@ -128,7 +141,10 @@ class CartTest(TestCase):
grind=CoffeeGrind.WHOLE, grind=CoffeeGrind.WHOLE,
update_quantity=False 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( cart.add(
request, request,
@ -137,7 +153,10 @@ class CartTest(TestCase):
grind=CoffeeGrind.WHOLE, grind=CoffeeGrind.WHOLE,
update_quantity=True 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): def test_cart_remove_item(self):
cart_detail_url = reverse('storefront:cart-detail') cart_detail_url = reverse('storefront:cart-detail')
@ -172,6 +191,3 @@ class CartTest(TestCase):
update_quantity=False update_quantity=False
) )
self.assertEqual(cart.get_total_weight(), Decimal(48)) self.assertEqual(cart.get_total_weight(), Decimal(48))
def test_cart_(self):
pass

View File

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

View File

@ -21,9 +21,9 @@ from . import RequestFaker
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CreateOrderTest(TestCase): class CreateOrderTest(TestCase):
def setUp(self): @classmethod
self.client = Client() def setUpTestData(cls):
self.product = Product.objects.create( cls.product = Product.objects.create(
name='Decaf', name='Decaf',
description='Coffee', description='Coffee',
sku='23987', sku='23987',
@ -32,6 +32,10 @@ class CreateOrderTest(TestCase):
visible_in_listings=True visible_in_listings=True
) )
def setUp(self):
self.client = Client()
def test_build_request_body(self): def test_build_request_body(self):
product_list_url = reverse('storefront:product-list') product_list_url = reverse('storefront:product-list')
response = self.client.get(product_list_url, follow=True) response = self.client.get(product_list_url, follow=True)

View File

@ -9,7 +9,7 @@ from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest
from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment
from accounts.models import User, Address from accounts.models import User, Address
from core.models import Product, Order from core.models import Product, Order, Coupon
from core import CoffeeGrind from core import CoffeeGrind
from storefront.forms import AddressForm, OrderCreateForm from storefront.forms import AddressForm, OrderCreateForm
from storefront.views import OrderCreateView, CheckoutAddressView from storefront.views import OrderCreateView, CheckoutAddressView
@ -17,7 +17,8 @@ from storefront.cart import Cart
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CheckoutAddressViewTest(TestCase):
class CheckoutAddressViewTests(TestCase):
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
@ -30,3 +31,48 @@ class CheckoutAddressViewTest(TestCase):
response = self.client.get(reverse('storefront:checkout-address')) response = self.client.get(reverse('storefront:checkout-address'))
self.assertTrue(response.context['form']) self.assertTrue(response.context['form'])
self.assertTrue(isinstance(response.context['form'], AddressForm)) 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)

View File

@ -6,10 +6,12 @@ from django.utils import timezone
from django.shortcuts import render, reverse, redirect, get_object_or_404 from django.shortcuts import render, reverse, redirect, get_object_or_404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.core.mail import EmailMessage 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.http import JsonResponse, HttpResponseRedirect
from django.views.generic.base import RedirectView, TemplateView 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.detail import DetailView, SingleObjectMixin
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.contrib.auth.decorators import login_required 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.models import User, Address
from accounts.utils import get_or_create_customer 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.models import Product, Order, Transaction, OrderLine, Coupon
from core.forms import ShippingMethodForm from core.forms import ShippingMethodForm
from core import OrderStatus 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 .cart import Cart
from .payments import CaptureOrder from .payments import CaptureOrder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CartView(TemplateView): class CartView(TemplateView):
template_name = 'storefront/cart_detail.html' template_name = 'storefront/cart_detail.html'
@ -53,6 +61,7 @@ class CartView(TemplateView):
context['coupon_apply_form'] = CouponApplyForm() context['coupon_apply_form'] = CouponApplyForm()
return context return context
class CartAddProductView(SingleObjectMixin, FormView): class CartAddProductView(SingleObjectMixin, FormView):
model = Product model = Product
form_class = AddToCartForm form_class = AddToCartForm
@ -118,7 +127,7 @@ class CouponApplyView(FormView):
def form_valid(self, form): def form_valid(self, form):
today = timezone.localtime(timezone.now()).date() today = timezone.localtime(timezone.now()).date()
code = form.cleaned_data['code'] code = form.cleaned_data['code'].upper()
try: try:
coupon = Coupon.objects.get( coupon = Coupon.objects.get(
code__iexact=code, code__iexact=code,
@ -200,6 +209,7 @@ class CheckoutAddressView(FormView):
self.request.session['shipping_address'] = address self.request.session['shipping_address'] = address
return super().form_valid(form) return super().form_valid(form)
class OrderCreateView(CreateView): class OrderCreateView(CreateView):
model = Order model = Order
template_name = 'storefront/order_form.html' template_name = 'storefront/order_form.html'
@ -212,8 +222,17 @@ class OrderCreateView(CreateView):
return HttpResponseRedirect( return HttpResponseRedirect(
reverse('storefront:checkout-address') reverse('storefront:checkout-address')
) )
else: elif self.request.session.get('coupon_code'):
return super().get(request, *args, **kwargs) 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): def get_initial(self):
cart = Cart(self.request) cart = Cart(self.request)
@ -249,6 +268,13 @@ class OrderCreateView(CreateView):
shipping_address = self.request.session.get('shipping_address') 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.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address)
form.instance.status = OrderStatus.DRAFT 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() self.object = form.save()
bulk_list = cart.build_bulk_list(self.object) bulk_list = cart.build_bulk_list(self.object)
objs = OrderLine.objects.bulk_create(bulk_list) objs = OrderLine.objects.bulk_create(bulk_list)
@ -260,10 +286,11 @@ class OrderCreateView(CreateView):
return JsonResponse(data) return JsonResponse(data)
@csrf_exempt @csrf_exempt
@require_POST @require_POST
def paypal_order_transaction_capture(request, transaction_id): def paypal_order_transaction_capture(request, transaction_id):
if request.method =="POST": if request.method == "POST":
data = CaptureOrder().capture_order(transaction_id) data = CaptureOrder().capture_order(transaction_id)
cart = Cart(request) cart = Cart(request)
cart.clear() cart.clear()
@ -280,6 +307,7 @@ def paypal_order_transaction_capture(request, transaction_id):
else: else:
return JsonResponse({'details': 'invalid request'}) return JsonResponse({'details': 'invalid request'})
@csrf_exempt @csrf_exempt
@require_POST @require_POST
def paypal_webhook_endpoint(request): def paypal_webhook_endpoint(request):
@ -291,39 +319,66 @@ def paypal_webhook_endpoint(request):
class PaymentDoneView(TemplateView): class PaymentDoneView(TemplateView):
template_name = 'storefront/payment_done.html' template_name = 'storefront/payment_done.html'
class PaymentCanceledView(TemplateView): class PaymentCanceledView(TemplateView):
template_name = 'storefront/payment_canceled.html' template_name = 'storefront/payment_canceled.html'
class CustomerDetailView(LoginRequiredMixin, DetailView): class CustomerDetailView(UserPassesTestMixin, LoginRequiredMixin, DetailView):
model = User model = User
template_name = 'storefront/customer_detail.html' template_name = 'storefront/customer_detail.html'
context_object_name = 'customer' 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 model = User
template_name = 'storefront/customer_form.html' template_name = 'storefront/customer_form.html'
context_object_name = 'customer' context_object_name = 'customer'
form_class = CustomerUpdateForm 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): 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 model = Order
template_name = 'storefront/order_detail.html' template_name = 'storefront/order_detail.html'
pk_url_kwarg = 'order_pk' 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['customer'] = User.objects.get(pk=self.kwargs['pk']) context['customer'] = User.objects.get(pk=self.kwargs['pk'])
return context return context
class CustomerAddressCreateView(LoginRequiredMixin, CreateView):
class CustomerAddressCreateView(
UserPassesTestMixin, LoginRequiredMixin, CreateView
):
model = Address model = Address
template_name = 'storefront/address_create_form.html' template_name = 'storefront/address_create_form.html'
form_class = AccountAddressForm 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -338,13 +393,23 @@ class CustomerAddressCreateView(LoginRequiredMixin, CreateView):
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): 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 model = Address
pk_url_kwarg = 'address_pk' pk_url_kwarg = 'address_pk'
template_name = 'storefront/address_form.html' template_name = 'storefront/address_form.html'
form_class = AccountAddressForm 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): def get_context_data(self, **kwargs):
context = super().get_context_data(**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']}) return reverse('storefront:customer-detail', kwargs={'pk': self.kwargs['pk']})
class AboutView(TemplateView): class AboutView(TemplateView):
template_name = 'storefront/about.html' template_name = 'storefront/about.html'
class FairTradeView(TemplateView): class FairTradeView(TemplateView):
template_name = 'storefront/fairtrade.html' template_name = 'storefront/fairtrade.html'
class ReviewListView(TemplateView): class ReviewListView(TemplateView):
template_name = 'storefront/reviews.html' template_name = 'storefront/reviews.html'
class ContactFormView(FormView, SuccessMessageMixin): class ContactFormView(FormView, SuccessMessageMixin):
template_name = 'storefront/contact_form.html' template_name = 'storefront/contact_form.html'
form_class = ContactForm form_class = ContactForm

7
src/templates/400.html Normal file
View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block content %}
<article class="error-view">
<h1>400 Bad request</h1>
</article>
{% endblock %}

7
src/templates/403.html Normal file
View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block content %}
<article class="error-view">
<h1>403 Forbidden</h1>
</article>
{% endblock %}

7
src/templates/404.html Normal file
View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block content %}
<article class="error-view">
<h1>404 Page not found</h1>
</article>
{% endblock %}

7
src/templates/500.html Normal file
View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block content %}
<article class="error-view">
<h1>500 Server error</h1>
</article>
{% endblock %}