From ac68daf0b68c96a2c574c2b3827a34ebaace03c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Tue, 24 Apr 2018 18:06:19 +0200 Subject: [PATCH] Update admin. Fix some more tests. Add stock info to template. --- src/shop/admin.py | 10 +++++ src/shop/models.py | 32 ++++++++++++---- src/shop/templates/product_detail.html | 10 ++++- src/shop/templates/shop_index.html | 6 ++- src/shop/tests.py | 52 ++++++++++++++++++++++++-- 5 files changed, 97 insertions(+), 13 deletions(-) diff --git a/src/shop/admin.py b/src/shop/admin.py index 426ec96b..b4550f4c 100644 --- a/src/shop/admin.py +++ b/src/shop/admin.py @@ -90,6 +90,15 @@ def available_to(product): available_to.short_description = 'Available to' +def stock_info(product): + if product.stock_amount: + return "{} / {}".format( + product.left_in_stock, + product.stock_amount + ) + return "N/A" + + @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = [ @@ -98,6 +107,7 @@ class ProductAdmin(admin.ModelAdmin): 'ticket_type', 'price', 'description', + stock_info, available_from, available_to ] diff --git a/src/shop/models.py b/src/shop/models.py index a3ab1329..adf97027 100644 --- a/src/shop/models.py +++ b/src/shop/models.py @@ -349,10 +349,16 @@ class Product(CreatedUpdatedModel, UUIDModel): - Whether now is in the self.available_in - If a stock is defined, that there are items left """ + predicates = [self.is_time_available] + if self.stock_amount: + predicates.append(self.is_stock_available) + return all(predicates) + + @property + def is_time_available(self): now = timezone.now() time_available = now in self.available_in - stock_available = self.left_in_stock() > 0 - return time_available and stock_available + return time_available def is_old(self): now = timezone.now() @@ -364,16 +370,26 @@ class Product(CreatedUpdatedModel, UUIDModel): now = timezone.now() return self.available_in.lower > now + @property def left_in_stock(self): - sold = OrderProductRelation.objects.filter( - product=self, - order__paid=True, - ).aggregate(Sum('quantity'))['quantity__sum'] + if self.stock_amount: + sold = OrderProductRelation.objects.filter( + product=self, + order__paid=True, + ).aggregate(Sum('quantity'))['quantity__sum'] - total_left = self.stock_amount - (sold or 0) + total_left = self.stock_amount - (sold or 0) - return total_left + return total_left + return None + @property + def is_stock_available(self): + if self.stock_amount: + stock_available = self.left_in_stock > 0 + return stock_available + # If there is no stock defined the product is generally available. + return True class OrderProductRelation(CreatedUpdatedModel): order = models.ForeignKey('shop.Order', on_delete=models.PROTECT) diff --git a/src/shop/templates/product_detail.html b/src/shop/templates/product_detail.html index 25799e63..e73081c4 100644 --- a/src/shop/templates/product_detail.html +++ b/src/shop/templates/product_detail.html @@ -25,15 +25,21 @@
- {% if product.stock_amount %} + {% if product.stock_amount and product.is_time_available %}

Availability
+ {% if product.left_in_stock > 0 %} {{ product.left_in_stock }} available
+ {% else %} + Sold out. + {% endif %}


{% endif %} + {% if product.is_stock_available %} +

Add to order

{% if user.is_authenticated %} @@ -67,6 +73,8 @@ to order this product {% endif %} + {% endif %} + diff --git a/src/shop/templates/shop_index.html b/src/shop/templates/shop_index.html index b9f805ba..c0915e5d 100644 --- a/src/shop/templates/shop_index.html +++ b/src/shop/templates/shop_index.html @@ -47,7 +47,6 @@ Shop | {{ block.super }} - {% endifchanged %} @@ -56,6 +55,11 @@ Shop | {{ block.super }} {{ product.name }} + {% if product.stock_amount and product.left_in_stock <= 10 %} +
+ Only {{ product.left_in_stock }} left! +
+ {% endif %}
diff --git a/src/shop/tests.py b/src/shop/tests.py index daca2dd5..809f2a5e 100644 --- a/src/shop/tests.py +++ b/src/shop/tests.py @@ -1,4 +1,7 @@ from django.test import TestCase +from django.utils import timezone + +from psycopg2.extras import DateTimeTZRange from .factories import ( ProductFactory, @@ -13,8 +16,8 @@ class ProductAvailabilityTest(TestCase): def test_product_available_by_stock(self): """ If no orders have been made, the product is still available. """ product = ProductFactory(stock_amount=10) - self.assertEqual(product.left_in_stock(), 10) - self.assertTrue(product.is_available) + self.assertEqual(product.left_in_stock, 10) + self.assertTrue(product.is_available()) def test_product_not_available_by_stock(self): """ If max orders have been made, the product is NOT available. """ @@ -26,5 +29,48 @@ class ProductAvailabilityTest(TestCase): order.paid = True order.save() - self.assertEqual(product.left_in_stock(), 0) + self.assertEqual(product.left_in_stock, 0) + self.assertFalse(product.is_stock_available) self.assertFalse(product.is_available()) + + def test_product_available_by_time(self): + """ The product is available if now is in the right timeframe. """ + product = ProductFactory() + # The factory defines the timeframe as now and 31 days forward. + self.assertTrue(product.is_time_available) + self.assertTrue(product.is_available()) + + def test_product_not_available_by_time(self): + """ The product is not available if now is outside the timeframe. """ + available_in = DateTimeTZRange( + lower=timezone.now() - timezone.timedelta(5), + upper=timezone.now() - timezone.timedelta(1) + ) + product = ProductFactory(available_in=available_in) + # The factory defines the timeframe as now and 31 days forward. + self.assertFalse(product.is_time_available) + self.assertFalse(product.is_available()) + + def test_product_is_not_available_yet(self): + """ The product is not available because we are before lower bound. """ + available_in = DateTimeTZRange( + lower=timezone.now() + timezone.timedelta(5) + ) + product = ProductFactory(available_in=available_in) + # Make sure there is no upper - just in case. + self.assertEqual(product.available_in.upper, None) + # The factory defines the timeframe as now and 31 days forward. + self.assertFalse(product.is_time_available) + self.assertFalse(product.is_available()) + + def test_product_is_available_from_now_on(self): + """ The product is available because we are after lower bound. """ + available_in = DateTimeTZRange( + lower=timezone.now() - timezone.timedelta(1) + ) + product = ProductFactory(available_in=available_in) + # Make sure there is no upper - just in case. + self.assertEqual(product.available_in.upper, None) + # The factory defines the timeframe as now and 31 days forward. + self.assertTrue(product.is_time_available) + self.assertTrue(product.is_available())