From 60896a2835eed58b5fc0b9e1a44112ea83347c12 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Wed, 11 May 2022 17:39:22 -0600 Subject: [PATCH] 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