Merge pull request #221 from bornhack/add_tests_to_shop

WIP Fix shop stock code and add tests.
This commit is contained in:
Víðir Valberg Guðmundsson 2018-04-24 18:13:43 +02:00 committed by GitHub
commit 35f01dddbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 97 additions and 14 deletions

View file

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

View file

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

View file

@ -25,15 +25,21 @@
<hr />
{% if product.stock_amount %}
{% if product.stock_amount and product.is_time_available %}
<h3>
<small>Availability</small><br />
{% if product.left_in_stock > 0 %}
<bold>{{ product.left_in_stock }}</bold> available<br />
{% else %}
<bold>Sold out.</bold>
{% endif %}
</h3>
<hr />
{% endif %}
{% if product.is_stock_available %}
<h3>Add to order</h3>
{% if user.is_authenticated %}
@ -67,6 +73,8 @@
to order this product
{% endif %}
{% endif %}
</div>
</div>

View file

@ -47,7 +47,6 @@ Shop | {{ block.super }}
</a>
</strong>
</td>
</td>
</tr>
{% endifchanged %}
@ -56,6 +55,11 @@ Shop | {{ block.super }}
<a href="{% url 'shop:product_detail' slug=product.slug %}">
{{ product.name }}
</a>
{% if product.stock_amount and product.left_in_stock <= 10 %}
<div class="label label-danger">
Only {{ product.left_in_stock }} left!
</div>
{% endif %}
</td>
<td>
<div class="pull-right">

View file

@ -1,8 +1,10 @@
from django.test import TestCase
from django.utils import timezone
from psycopg2.extras import DateTimeTZRange
from .factories import (
ProductFactory,
OrderFactory,
OrderProductRelationFactory,
)
@ -13,8 +15,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 +28,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())