From 60896a2835eed58b5fc0b9e1a44112ea83347c12 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Wed, 11 May 2022 17:39:22 -0600 Subject: [PATCH 1/3] pep8 --- src/accounts/fixtures/accounts.json | 66 +++++++++++++++++++++- src/core/models.py | 25 +++++---- src/functional_tests/test_home.py | 4 +- src/static/styles/main.css | 5 ++ src/storefront/forms.py | 15 ++++- src/storefront/tests/test_cart.py | 51 +++++++++++------ src/storefront/tests/test_payments.py | 10 +++- src/storefront/views.py | 79 ++++++++++++++++++++++----- 8 files changed, 204 insertions(+), 51 deletions(-) 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/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_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/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..27970b1 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', @@ -89,7 +94,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 +108,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 +121,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 +142,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 +154,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 +192,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_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/views.py b/src/storefront/views.py index 5adae4b..ca9a0f2 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' @@ -260,10 +270,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 +291,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 +303,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 +377,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 +404,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 From 058d682ddb4d00391cf1dd54d8c9217aaba9ce34 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Wed, 11 May 2022 17:39:33 -0600 Subject: [PATCH 2/3] Error templates --- src/templates/400.html | 7 +++++++ src/templates/403.html | 7 +++++++ src/templates/404.html | 7 +++++++ src/templates/500.html | 7 +++++++ 4 files changed, 28 insertions(+) create mode 100644 src/templates/400.html create mode 100644 src/templates/403.html create mode 100644 src/templates/404.html create mode 100644 src/templates/500.html 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 %} From 4f43cc6a80f4622bdc75557b133a6940c97a0b28 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Wed, 11 May 2022 20:09:45 -0600 Subject: [PATCH 3/3] Add coupon check --- src/core/fixtures/coupons.json | 29 +++++++ src/core/migrations/0009_coupon_users.py | 20 +++++ src/functional_tests/test_address.py | 4 - src/functional_tests/test_coupon.py | 100 +++++++++++++++++++++++ src/storefront/cart.py | 5 +- src/storefront/tests/test_cart.py | 1 - src/storefront/tests/test_models.py | 18 ++++ src/storefront/tests/test_views.py | 50 +++++++++++- src/storefront/views.py | 20 ++++- 9 files changed, 237 insertions(+), 10 deletions(-) create mode 100644 src/core/fixtures/coupons.json create mode 100644 src/core/migrations/0009_coupon_users.py create mode 100644 src/functional_tests/test_coupon.py create mode 100644 src/storefront/tests/test_models.py 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/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/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/tests/test_cart.py b/src/storefront/tests/test_cart.py index 27970b1..5ea93de 100644 --- a/src/storefront/tests/test_cart.py +++ b/src/storefront/tests/test_cart.py @@ -61,7 +61,6 @@ class CartTest(TestCase): request = response.wsgi_request cart = Cart(request) - cart = Cart(request) cart.add( request, product=self.product, 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_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 ca9a0f2..63da3e6 100644 --- a/src/storefront/views.py +++ b/src/storefront/views.py @@ -222,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) @@ -259,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)