diff --git a/ecommerce/asgi.py b/ecommerce/asgi.py index 65e06ff..b65570a 100644 --- a/ecommerce/asgi.py +++ b/ecommerce/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ecommerce.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ecommerce.settings") application = get_asgi_application() diff --git a/ecommerce/settings/dev.py b/ecommerce/settings/dev.py index 6f2a967..a8a8edd 100644 --- a/ecommerce/settings/dev.py +++ b/ecommerce/settings/dev.py @@ -41,4 +41,4 @@ STATIC_URL = "static/" MEDIA_URL = "media/" STATIC_ROOT = os.path.join("static") -MEDIA_ROOT = os.path.join("media") \ No newline at end of file +MEDIA_ROOT = os.path.join("media") diff --git a/ecommerce/settings/prod.py b/ecommerce/settings/prod.py index 90a02be..49e471c 100644 --- a/ecommerce/settings/prod.py +++ b/ecommerce/settings/prod.py @@ -38,6 +38,5 @@ "staticfiles": { "BACKEND": "storages.backends.s3boto3.S3ManifestStaticStorage", "LOCATION": "static", - } + }, } - diff --git a/ecommerce/wsgi.py b/ecommerce/wsgi.py index 5196099..86a2d0b 100644 --- a/ecommerce/wsgi.py +++ b/ecommerce/wsgi.py @@ -16,9 +16,9 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -#Take environment variables from .env file -environ.Env.read_env(os.path.join(BASE_DIR, '.env')) +# Take environment variables from .env file +environ.Env.read_env(os.path.join(BASE_DIR, ".env")) -os.environ.setdefault('DJANGO_SETTINGS_MODULE', env("WORKING_ENV")) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("WORKING_ENV")) application = get_wsgi_application() diff --git a/store/apps.py b/store/apps.py index 41658c1..1a5be03 100644 --- a/store/apps.py +++ b/store/apps.py @@ -2,5 +2,5 @@ class StoreConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'store' + default_auto_field = "django.db.models.BigAutoField" + name = "store" diff --git a/store/context_processors.py b/store/context_processors.py index 61492c9..814ed9b 100644 --- a/store/context_processors.py +++ b/store/context_processors.py @@ -1,3 +1,3 @@ def bag_items_processor(request): - total_items = request.session.get('total_items', 0) - return {'total_items': total_items} \ No newline at end of file + total_items = request.session.get("total_items", 0) + return {"total_items": total_items} diff --git a/store/forms.py b/store/forms.py index 8c64323..ce3c585 100644 --- a/store/forms.py +++ b/store/forms.py @@ -8,6 +8,7 @@ class Meta: model = Store exclude = ["owner"] + class OrderForm(forms.ModelForm): class Meta: model = Order diff --git a/store/models.py b/store/models.py index 914b911..2d4677e 100644 --- a/store/models.py +++ b/store/models.py @@ -12,7 +12,9 @@ class Store(models.Model): category = models.CharField(max_length=30) city = models.CharField(max_length=25) state = models.CharField(max_length=25) - image = models.ImageField(upload_to=StoreUtils.generate_store_image_path, blank=True) + image = models.ImageField( + upload_to=StoreUtils.generate_store_image_path, blank=True + ) objects = StoreManager() @@ -39,7 +41,9 @@ def __str__(self): class Order(models.Model): store = models.ForeignKey(Store, on_delete=models.CASCADE) products = models.ManyToManyField(Product, through="OrderItem") - total_cost = models.DecimalField(max_digits=10, decimal_places=2, validators=[MinValueValidator(1.00)]) + total_cost = models.DecimalField( + max_digits=10, decimal_places=2, validators=[MinValueValidator(1.00)] + ) paid_on = models.DateTimeField(null=True) first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -59,7 +63,7 @@ def save(self, *args, **kwargs): def __str__(self): return f"Order #{self.id}" - + class OrderItem(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE) @@ -68,12 +72,14 @@ class OrderItem(models.Model): def __str__(self): return f"{self.quantity} of {self.product.name} in {self.order}" - + def save(self, *args, **kwargs): self.full_clean() super().save(*args, **kwargs) order = self.order - total_cost = sum(order_item.quantity * order_item.product.price for order_item in order.orderitem_set.all()) + total_cost = sum( + order_item.quantity * order_item.product.price + for order_item in order.orderitem_set.all() + ) order.total_cost = total_cost order.save() - \ No newline at end of file diff --git a/store/store_utils.py b/store/store_utils.py index 54ff0bf..38fc767 100644 --- a/store/store_utils.py +++ b/store/store_utils.py @@ -2,6 +2,7 @@ class StoreUtils: """ Misc utilities for keeping logic out of models, views, forms, etc. """ + @staticmethod def generate_store_image_path(instance, filename): store_name = instance.name.replace(" ", "_").lower() diff --git a/store/tests/factories/order_factory.py b/store/tests/factories/order_factory.py index 4c75184..7b87f6a 100644 --- a/store/tests/factories/order_factory.py +++ b/store/tests/factories/order_factory.py @@ -1,5 +1,3 @@ -from decimal import Decimal -import random import factory from store.models import Order diff --git a/store/tests/factories/store_factory.py b/store/tests/factories/store_factory.py index d5fa3bd..15f2cb0 100644 --- a/store/tests/factories/store_factory.py +++ b/store/tests/factories/store_factory.py @@ -9,6 +9,7 @@ faker: Faker = Faker() faker.add_provider(faker_commerce.Provider) + class StoreFactory(factory.django.DjangoModelFactory): class Meta: model = Store diff --git a/store/tests/views/product_admin/test_product_admin_modify_view.py b/store/tests/views/product_admin/test_product_admin_modify_view.py index 2cb9de0..debdcd8 100644 --- a/store/tests/views/product_admin/test_product_admin_modify_view.py +++ b/store/tests/views/product_admin/test_product_admin_modify_view.py @@ -6,6 +6,7 @@ from store.tests.factories.store_factory import StoreFactory from store.tests.factories.user_factory import UserFactory + class TestProductAdminModifyView(TestCase): def setUp(self): self.owner = UserFactory() @@ -16,5 +17,7 @@ def setUp(self): def test_customer_cannot_modify_products(self): self.client.login(username=self.customer.username, password="") - response = self.client.get(reverse("store:product_admin_modify", kwargs={"pk": self.product.pk})) - self.assertIsInstance(response, HttpResponseForbidden) \ No newline at end of file + response = self.client.get( + reverse("store:product_admin_modify", kwargs={"pk": self.product.pk}) + ) + self.assertIsInstance(response, HttpResponseForbidden) diff --git a/store/tests/views/product_admin/test_product_admin_view.py b/store/tests/views/product_admin/test_product_admin_view.py index de4f23d..519aa9c 100644 --- a/store/tests/views/product_admin/test_product_admin_view.py +++ b/store/tests/views/product_admin/test_product_admin_view.py @@ -20,4 +20,4 @@ def test_admin_loads_first_store_when_multiple_owned(self): store = response.context["store"] self.assertEqual(store, self.correct_store) products = list(store.product_set.all()) - self.assertEqual(products, self.products) \ No newline at end of file + self.assertEqual(products, self.products) diff --git a/store/tests/views/test_add_to_bag_view.py b/store/tests/views/test_add_to_bag_view.py index 95e71df..ec46bc3 100644 --- a/store/tests/views/test_add_to_bag_view.py +++ b/store/tests/views/test_add_to_bag_view.py @@ -4,6 +4,7 @@ from django.urls import reverse from store.views import add_to_bag + class AddToBagView(TestCase): def setUp(self): self.client = Client() @@ -11,28 +12,38 @@ def setUp(self): self.session_store = SessionStore() def request_generator(self, product_id: int): - request: HttpRequest = self.factory.post('/add-to-bag/', {'product_id': product_id}) + request: HttpRequest = self.factory.post( + "/add-to-bag/", {"product_id": product_id} + ) request.session: SessionStore = self.session_store return request def test_request_succeeds_when_product_quantity_is_one(self): response = add_to_bag(self.request_generator(1)) - self.assertJSONEqual(response.content, {'status': 'success', 'total_items': 1}) + self.assertJSONEqual(response.content, {"status": "success", "total_items": 1}) def test_request_rejects_bad_product_id(self): with self.assertRaises(ValueError): - add_to_bag(self.request_generator('bad id')) + add_to_bag(self.request_generator("bad id")) def test_request_increments_product_quantity(self): session = self.client.session - session['bag'] = [{'product_id': 1, 'quantity': 1}] + session["bag"] = [{"product_id": 1, "quantity": 1}] session.save() - response: JsonResponse = self.client.post(reverse('store:add-to-bag'), {'product_id': 1}) + response: JsonResponse = self.client.post( + reverse("store:add-to-bag"), {"product_id": 1} + ) self.assertEqual(response.status_code, 200) - bag: list = self.client.session['bag'] # Fetch the updated session data - product_in_bag: dict = next((item for item in bag if item['product_id'] == 1), None) - - self.assertIsNotNone(product_in_bag, 'The product should exist in the bag') - self.assertEqual(product_in_bag['quantity'], 2, "The quantity for this product should've been incremented.") \ No newline at end of file + bag: list = self.client.session["bag"] # Fetch the updated session data + product_in_bag: dict = next( + (item for item in bag if item["product_id"] == 1), None + ) + + self.assertIsNotNone(product_in_bag, "The product should exist in the bag") + self.assertEqual( + product_in_bag["quantity"], + 2, + "The quantity for this product should've been incremented.", + ) diff --git a/store/tests/views/test_checkout_view.py b/store/tests/views/test_checkout_view.py index d440710..7064bdc 100644 --- a/store/tests/views/test_checkout_view.py +++ b/store/tests/views/test_checkout_view.py @@ -2,7 +2,6 @@ from django.urls import reverse from faker import Faker -from store.models import Order from store.tests.factories.order_item_factory import OrderItemFactory from store.tests.factories.product_factory import ProductFactory from store.tests.factories.store_factory import StoreFactory diff --git a/store/tests/views/test_create_store_view.py b/store/tests/views/test_create_store_view.py index 2afc0a0..fbb3f1d 100644 --- a/store/tests/views/test_create_store_view.py +++ b/store/tests/views/test_create_store_view.py @@ -12,7 +12,6 @@ class CreateStoreViewTest(TestCase): - def setUp(self): self.owner = UserFactory() @@ -24,10 +23,10 @@ def setUp(self): "city": faker.city(), "state": faker.state(), "image": SimpleUploadedFile( - name="test_image.jpeg", - content=open("media/tests/test_file.png", "rb").read(), - content_type="image/jpeg", - ) + name="test_image.jpeg", + content=open("media/tests/test_file.png", "rb").read(), + content_type="image/jpeg", + ), } self.response = self.client.post(reverse("store:create_store"), data=self.data) @@ -36,7 +35,7 @@ def test_create_store_success(self): self.assertEqual(self.response.status_code, 302) store = Store.objects.for_user_admin(self.owner) self.assertTrue(store) - + def test_redirected_when_not_logged_in(self): self.client.logout() response = self.client.get(reverse("store:create_store")) diff --git a/store/tests/views/test_store_list_view.py b/store/tests/views/test_store_list_view.py index ff6a4b8..8c9b3df 100644 --- a/store/tests/views/test_store_list_view.py +++ b/store/tests/views/test_store_list_view.py @@ -3,15 +3,14 @@ from store.tests.factories.store_factory import StoreFactory + class StoreListView(TestCase): def setUp(self): self.stores = StoreFactory.create_batch(10) - self.response = self.client.get(reverse('store:store_list')) + self.response = self.client.get(reverse("store:store_list")) def test_stores_in_context(self): - self.assertEqual(self.response.status_code, 200) response_queryset = self.response.context["stores"] self.assertEqual(len(response_queryset), 10) - \ No newline at end of file diff --git a/store/tests/views/test_stripe_webhook_view.py b/store/tests/views/test_stripe_webhook_view.py index 41d5676..cf620a6 100644 --- a/store/tests/views/test_stripe_webhook_view.py +++ b/store/tests/views/test_stripe_webhook_view.py @@ -15,19 +15,21 @@ def setUp(self): "object": { "id": "cs_test_session", "object": "checkout.session", - "metadata": { - "session_key": self.client.session.session_key - } + "metadata": {"session_key": self.client.session.session_key}, } - } + }, } - + @patch("stripe.Event.construct_from") def test_stripe_webhook_success(self, mock_construct_event): mock_construct_event.return_value = self.mock_event # Make a POST request to the webhook endpoint - response = self.client.post(reverse("store:stripe_webhook"), data=json.dumps(self.mock_event), content_type="application/json") + response = self.client.post( + reverse("store:stripe_webhook"), + data=json.dumps(self.mock_event), + content_type="application/json", + ) # Verify that the webhook endpoint returns a 200 response self.assertEqual(response.status_code, 200) @@ -44,7 +46,11 @@ def test_stripe_webhook_failure(self, mock_construct_event): mock_construct_event.return_value = self.mock_event # Make a POST request to the webhook endpoint - response = self.client.post(reverse("store:stripe_webhook"), data=json.dumps(self.mock_event), content_type="application/json") - + response = self.client.post( + reverse("store:stripe_webhook"), + data=json.dumps(self.mock_event), + content_type="application/json", + ) + # Verify that the session isn't cleared if there's a bad session key - self.assertEqual(response.status_code, 400) \ No newline at end of file + self.assertEqual(response.status_code, 400) diff --git a/store/urls.py b/store/urls.py index 489ef9a..7c7a47a 100644 --- a/store/urls.py +++ b/store/urls.py @@ -28,7 +28,9 @@ path("", StoreList.as_view(), name="store_list"), path("store/product/", ProductDetail.as_view(), name="product"), path("checkout/", checkout, name="checkout"), - path("create-payment-session", create_payment_session, name="create_payment_session"), + path( + "create-payment-session", create_payment_session, name="create_payment_session" + ), path("stripe-webhook", stripe_webhook, name="stripe_webhook"), path("add-to-bag/", add_to_bag, name="add-to-bag"), path("user-admin/store/products", ProductAdmin.as_view(), name="product_admin"), @@ -51,31 +53,31 @@ path( "user-admin/reports/customers/csv", DownloadCustomerReport.as_view(), - name="download_customer_report" + name="download_customer_report", ), path( "user-admin/reports/customers/pdf", DownloadCustomerPDFReport.as_view(), - name="download_customer_pdf_report" + name="download_customer_pdf_report", ), path( "user-admin/reports/products", DownloadProductReport.as_view(), - name="download_product_report" + name="download_product_report", ), path( "user-admin/reports/products/pdf", DownloadProductPDFReport.as_view(), - name="download_product_pdf_report" + name="download_product_pdf_report", ), path( "user-admin/reports/sales", DownloadSalesReport.as_view(), - name="download_sales_report" + name="download_sales_report", ), path( "user-admin/reports/sales/pdf", DownloadSalesPDFReport.as_view(), - name="download_sales_pdf_report" + name="download_sales_pdf_report", ), ] diff --git a/store/view_utils.py b/store/view_utils.py index 5ffa8bc..decd8a6 100644 --- a/store/view_utils.py +++ b/store/view_utils.py @@ -73,6 +73,7 @@ def get_order_items_by_store(products_in_bag): return stores + def create_orders_for_stores(stores, order_info): orders = [] @@ -89,4 +90,3 @@ def create_orders_for_stores(stores, order_info): ) orders.append(order) return orders - diff --git a/store/views.py b/store/views.py index 281539e..79ec886 100644 --- a/store/views.py +++ b/store/views.py @@ -82,13 +82,15 @@ def checkout(request): if stripe.api_key: request.session["order_info"] = form.cleaned_data - return create_payment_session(request, request.session.session_key, products_in_bag) + return create_payment_session( + request, request.session.session_key, products_in_bag + ) else: create_orders_for_stores(stores, form.cleaned_data) messages.add_message( request, messages.INFO, - "Order(s) succuessfully placed. Complete payment to fulfill your order." + "Order(s) succuessfully placed. Complete payment to fulfill your order.", ) request.session["bag"] = [] @@ -181,11 +183,10 @@ def stripe_webhook(request): metadata = session["metadata"] session_key = metadata["session_key"] - try: if session_key == "" or session_key is None: raise StripeWebHookException() - + else: # assign the current request session to the user's and not Stripe. request.session = SessionStore(session_key=session_key) @@ -206,9 +207,9 @@ def stripe_webhook(request): return HttpResponse(status=200) except StripeWebHookException: messages.add_message( - request, - messages.ERROR, - "An issue occurred in the checkout process. Please try again." + request, + messages.ERROR, + "An issue occurred in the checkout process. Please try again.", ) redirect(reverse("store:checkout")) return HttpResponse(status=400) @@ -270,8 +271,10 @@ def product_admin_modify(request, pk): product = get_object_or_404(Product, pk=pk) if product.store.owner != request.user: - return HttpResponseForbidden("You don't have permission to modify this product.") - + return HttpResponseForbidden( + "You don't have permission to modify this product." + ) + form = ProductAdminForm(instance=product) if request.method == "POST": @@ -294,7 +297,9 @@ def product_admin_modify(request, pk): ) return HttpResponseRedirect(reverse("store:product_admin")) - return render(request, "store/user-admin/product/product_admin_modify.html", {"form": form}) + return render( + request, "store/user-admin/product/product_admin_modify.html", {"form": form} + ) class ProductAdminAdd(LoginRequiredMixin, CreateView): @@ -357,7 +362,9 @@ def order_admin_modify(request, pk): return HttpResponseRedirect(reverse("store:order_admin")) return render( - request, "store/user-admin/order/order_admin_modify.html", {"form": form, "order": order} + request, + "store/user-admin/order/order_admin_modify.html", + {"form": form, "order": order}, ) @@ -388,15 +395,14 @@ def get(self, request, *args, **kwargs): store = Store.objects.for_user_admin(self.request.user) orders: QuerySet[Order] = Order.objects.filter(store=store) - data = { - "store": store, - "customers": orders - } + data = {"store": store, "customers": orders} current_datetime = timezone.now() str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S") - return self.generate_pdf_report(f"customer_list_{str_datetime}", "store/reports/customer.html", data) + return self.generate_pdf_report( + f"customer_list_{str_datetime}", "store/reports/customer.html", data + ) class DownloadProductReport(LoginRequiredMixin, ReportingMixin, View): @@ -411,7 +417,7 @@ def get(self, request, *args, **kwargs): product.name, product.rating, product.price, - product.description + product.description, ) for product in products ] @@ -419,33 +425,38 @@ def get(self, request, *args, **kwargs): str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S") return self.generate_csv_report(f"product_list_{str_datetime}", header, data) - + class DownloadProductPDFReport(LoginRequiredMixin, ReportingMixin, View): def get(self, request, *args, **kwargs): store = Store.objects.for_user_admin(self.request.user) products: QuerySet[Product] = Product.objects.filter(store=store) - data = { - "store": store, - "products": products - } + data = {"store": store, "products": products} current_datetime = timezone.now() str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S") - return self.generate_pdf_report(f"product_list_{str_datetime}", "store/reports/product.html", data) - + return self.generate_pdf_report( + f"product_list_{str_datetime}", "store/reports/product.html", data + ) + class DownloadSalesReport(LoginRequiredMixin, ReportingMixin, View): def get(self, request, *args, **kwargs): store = Store.objects.for_user_admin(self.request.user) order_items: QuerySet[OrderItem] = OrderItem.objects.filter(order__store=store) - - header = ["order_id", "product_name", - "product_rating", "product_price", "quantity", - "total_quantity_cost", "percent_of_total_order", "total_order_cost"] + header = [ + "order_id", + "product_name", + "product_rating", + "product_price", + "quantity", + "total_quantity_cost", + "percent_of_total_order", + "total_order_cost", + ] data = [ ( order_item.order.id, @@ -463,8 +474,10 @@ def get(self, request, *args, **kwargs): current_datetime = timezone.now() str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S") - return self.generate_csv_report(f"{store.name.lower()}_sales_report_{str_datetime}", header, data) - + return self.generate_csv_report( + f"{store.name.lower()}_sales_report_{str_datetime}", header, data + ) + class DownloadSalesPDFReport(LoginRequiredMixin, ReportingMixin, View): def get(self, request, *args, **kwargs): @@ -479,26 +492,31 @@ def get(self, request, *args, **kwargs): if order_id not in order_cost_map: order_cost_map[order_id] = 0 - order_data.append({ - "order_id": order_id, - "product_name": order_item.product.name, - "product_rating": order_item.product.rating, - "product_price": order_item.product.price, - "quantity": order_item.quantity, - "total_quantity_cost": total_quantity_cost, - "percent_of_total_order": round((total_quantity_cost / order_item.order.total_cost) * 100), - "order_cost": order_item.order.total_cost, - }) - - data = { - "store": store, - "order_item_data": order_data - } + order_data.append( + { + "order_id": order_id, + "product_name": order_item.product.name, + "product_rating": order_item.product.rating, + "product_price": order_item.product.price, + "quantity": order_item.quantity, + "total_quantity_cost": total_quantity_cost, + "percent_of_total_order": round( + (total_quantity_cost / order_item.order.total_cost) * 100 + ), + "order_cost": order_item.order.total_cost, + } + ) + + data = {"store": store, "order_item_data": order_data} current_datetime = timezone.now() str_datetime = current_datetime.strftime("%d_%m_%Y_%H:%M:%S") - return self.generate_pdf_report(f"{store.name.lower()}_sales_report_{str_datetime}", "store/reports/sales.html", data) + return self.generate_pdf_report( + f"{store.name.lower()}_sales_report_{str_datetime}", + "store/reports/sales.html", + data, + ) class CreateStore(LoginRequiredMixin, CreateView):