Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion src/shop/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ def available_for_days(self):

@property
def left_in_stock(self):
if self.stock_amount:
if self.stock_amount is not None:
# All orders that are not open and not cancelled count towards what has
# been "reserved" from stock.
#
Expand All @@ -604,6 +604,42 @@ def is_stock_available(self):
# If there is no stock defined the product is generally available.
return True

@property
def labels(self) -> list:
"""Return list of label objects for this product."""
labels = []

if self.sub_products.all().exists():
labels.append({
"type": "bundle",
"text": "Bundle",
})

if self.stock_amount is not None:
if self.left_in_stock < 1 or not self.is_time_available:
labels.insert(0, {
"type": "sold_out",
"text": "Sold out!",
})

# Sold out is an exclusive state - no further labels apply
return labels

elif self.left_in_stock <= 10:
labels.append({
"type": "low_stock",
"text": f"Only {self.left_in_stock} left!",
})

if self.available_for_days < 20:
labels.append({
"type": "ending_soon",
"text": f"Sales end in {self.available_for_days} days!",
})

return labels



class SubProductRelation(
ExportModelOperationsMixin("sub_product_relation"),
Expand Down
3 changes: 3 additions & 0 deletions src/shop/templates/labels/default.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{% load label_mapping %}

<span class="badge {{ label.type|css_class }}">{{ label.text }}</span>
42 changes: 5 additions & 37 deletions src/shop/templates/shop_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,43 +58,11 @@
{{ product.name }}
</a>


{% if product.stock_amount %}
{% if product.left_in_stock <= 0 or not product.is_time_available %}
<div class="label label-danger" style="margin-left: 1em;"><!-- We can replace the style when we upgrade to Bootstrap 5. -->
Sold out!
</div>
{% else %}

{% if product.left_in_stock <= 10 %}
<div class="label label-info" style="margin-left: 1em;">
Only {{ product.left_in_stock }} left!
</div>
{% endif %}

{% endif %}

{% if product.left_in_stock > 0 %}
<div class="label label-info" style="margin-left: 1em;">
Sales end in {{ product.available_for_days }} days!
</dev>
{% endif %}

{% else %}

{% if product.available_for_days < 20 %}
<div class="label label-info" style="margin-left: 1em;">
Sales end in {{ product.available_for_days }} days!
</dev>
{% endif %}

{% endif %}

{% if product.has_subproducts %}
<div class="label label-info" style="margin-left: 1em;"><!-- We can replace the style when we upgrade to Bootstrap 5. -->
Bundle
</div>
{% endif %}
<div class="ms-3">
{% for label in product.labels %}
{% include 'labels/default.html' with label=label %}
{% endfor %}
</div>

</td>
<td>
Expand Down
Empty file.
15 changes: 15 additions & 0 deletions src/shop/templatetags/label_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django import template

register = template.Library()

@register.filter(name="css_class")
def css_class(value: str) -> str:
"""Templatetag for mapping a 'label_type' to a css class."""
map = {
"sold_out": "text-bg-danger",
"low_stock": "text-bg-warning",
"ending_soon": "text-bg-secondary",
"bundle": "text-bg-info",
}

return map.get(value, "text-bg-primary")
100 changes: 100 additions & 0 deletions src/shop/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,106 @@ def test_product_is_available_from_now_on(self):
self.assertTrue(product.is_available())


class ProductLabelsTest(TestCase):
"""Test logic about labels for products."""

def test_labels_for_product_not_available_by_stock(self):
"""Test product.labels returns a 'sold_out' label object."""
product = ProductFactory(stock_amount=1)
OrderProductRelationFactory(product=product, order__open=None)

result = product.labels[0]

assert result.get("type") == "sold_out"
assert result.get("text") == "Sold out!"

def test_labels_for_product_avoid_other_labels_when_sold_out(self):
"""Test product.labels not returning other labels when being sold out."""
available_in = DateTimeTZRange(
lower=timezone.now(),
upper=timezone.now() + timezone.timedelta(6),
)

# With available_in
product = ProductFactory(stock_amount=0, available_in=available_in)

result = product.labels[0]

assert len(product.labels) == 1
assert result.get("type") == "sold_out"
assert result.get("text") == "Sold out!"

# Without available_in
product = ProductFactory(stock_amount=0)

result = product.labels[0]

assert len(product.labels) == 1
assert result.get("type") == "sold_out"
assert result.get("text") == "Sold out!"

def test_labels_for_product_with_stock_below_or_equal_to_10(self):
"""Test the product.labels returns a 'low_stock' label object."""
product = ProductFactory(stock_amount=11)

# No label with stock_amount=11
assert len(product.labels) == 0

OrderProductRelationFactory(product=product, order__open=None)
result = product.labels[0]

assert result.get("type") == "low_stock"
assert result.get("text") == "Only 10 left!"

def test_labels_for_product_ending_within_20_days(self):
"""Test the product returns a 'ending_soon' label object."""
available_in = DateTimeTZRange(
lower=timezone.now(),
upper=timezone.now() + timezone.timedelta(6),
)

# With stock
product = ProductFactory(stock_amount=11, available_in=available_in)

result = product.labels[0]

assert result.get("type") == "ending_soon"
assert result.get("text") == "Sales end in 5 days!"

# Without stock
product = ProductFactory(available_in=available_in)

result = product.labels[0]

assert result.get("type") == "ending_soon"
assert result.get("text") == "Sales end in 5 days!"

def test_labels_for_product_is_empty_when_no_label_applies(self):
"""
Test the product.labels returns an empty list when no label applies.
"""
product = ProductFactory()

assert len(product.labels) == 0

def test_labels_for_product_when_being_a_bundle(self):
"""Test the product.labels when product has subproduct (bundle)."""
bundle_product = ProductFactory()
sub_product = ProductFactory(
ticket_type=TicketTypeFactory(single_ticket_per_product=False),
)
bundle_product.sub_products.add(
sub_product,
through_defaults={"number_of_tickets": 5},
)

result = bundle_product.labels[0]

assert len(bundle_product.labels) == 1
assert result.get("type") == "bundle"
assert result.get("text") == "Bundle"


class TestOrderProductRelationForm(TestCase):
def test_clean_quantity_succeeds_when_stock_not_exceeded(self):
product = ProductFactory(stock_amount=2)
Expand Down
Loading