Skip to content

Commit

Permalink
Merge pull request #4261 from django-oscar/excluded-categories
Browse files Browse the repository at this point in the history
[FEAT] Add excluded categories in range
  • Loading branch information
specialunderwear committed Apr 22, 2024
2 parents 992e1c6 + 5f4afca commit f09739a
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/oscar/apps/dashboard/ranges/forms.py
Expand Up @@ -22,6 +22,7 @@ class Meta:
"is_public",
"includes_all_products",
"included_categories",
"excluded_categories",
]


Expand Down
33 changes: 31 additions & 2 deletions src/oscar/apps/offer/abstract_models.py
Expand Up @@ -1003,6 +1003,16 @@ class AbstractRange(models.Model):
blank=True,
verbose_name=_("Included Categories"),
)
excluded_categories = models.ManyToManyField(
"catalogue.Category",
related_name="excludes",
blank=True,
verbose_name=_("Excluded Categories"),
help_text=_(
"Products with these categories are excluded from the range when "
"Includes all products is checked"
),
)

# Allow a custom range instance to be specified
proxy_class = fields.NullCharField(
Expand Down Expand Up @@ -1112,8 +1122,23 @@ def product_queryset(self):
Product = self.included_products.model

if self.includes_all_products:
_filter = Q(id__in=self.excluded_products.values("id"))
# extend filter if excluded_categories exist
if self.excluded_categories.exists():
expanded_range_categories = ExpandDownwardsCategoryQueryset(
self.excluded_categories.values("id")
)
_filter |= Q(categories__in=expanded_range_categories)
# extend filter for parent categories, exclude parent = None
if (
Product.objects.exclude(parent=None)
.filter(parent__categories__in=expanded_range_categories)
.exists()
):
_filter |= Q(parent__categories__in=expanded_range_categories)

# Filter out blacklisted
return Product.objects.exclude(id__in=self.excluded_products.values("id"))
return Product.objects.exclude(_filter)

# start with filter clause that always applies
_filter = Q(includes=self)
Expand Down Expand Up @@ -1162,7 +1187,11 @@ def is_reorderable(self):
"""
Test whether products for the range can be re-ordered.
"""
return not (self.included_categories.exists() or self.classes.exists())
return not (
self.included_categories.exists()
or self.classes.exists()
or (self.excluded_categories.exists() and self.includes_all_products)
)


class AbstractRangeProduct(models.Model):
Expand Down
24 changes: 24 additions & 0 deletions src/oscar/apps/offer/migrations/0013_range_excluded_categories.py
@@ -0,0 +1,24 @@
# Generated by Django 4.2.9 on 2024-03-22 19:20

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("catalogue", "0027_attributeoption_code_attributeoptiongroup_code_and_more"),
("offer", "0012_fixedunitdiscountbenefit_alter_benefit_type"),
]

operations = [
migrations.AddField(
model_name="range",
name="excluded_categories",
field=models.ManyToManyField(
blank=True,
help_text="Products with these categories are excluded from the range when Includes all products is checked",
related_name="excludes",
to="catalogue.category",
verbose_name="Excluded Categories",
),
),
]
16 changes: 15 additions & 1 deletion src/oscar/apps/offer/queryset.py
Expand Up @@ -23,6 +23,17 @@ def _excluded_products_clause(self, product):
)
return ~models.Q(excluded_products=product)

def _excluded_categories_clause(self, product):
if product.structure == product.CHILD:
# child products are excluded from a range if their parent contains
# category that is excluded
return ~(
models.Q(
excluded_categories__id__in=product.parent.categories.values("id")
)
)
return ~models.Q(excluded_categories__id__in=product.categories.values("id"))

def _included_products_clause(self, product):
if product.structure == product.CHILD:
# child products are included in a range if either they are
Expand Down Expand Up @@ -56,10 +67,13 @@ def contains_product(self, product):
# turned on, we only need to look at explicit exclusions, the other
# mechanism for adding a product to a range don't need to be checked
wide = self.filter(
self._excluded_products_clause(product), includes_all_products=True
self._excluded_products_clause(product),
self._excluded_categories_clause(product),
includes_all_products=True,
)
narrow = self.filter(
self._excluded_products_clause(product),
self._excluded_categories_clause(product),
self._included_products_clause(product)
| models.Q(
included_categories__in=ExpandUpwardsCategoryQueryset(
Expand Down
12 changes: 12 additions & 0 deletions tests/integration/offer/test_range.py
Expand Up @@ -11,6 +11,9 @@ def setUp(self):
name="All products", includes_all_products=True
)
self.prod = create_product()
self.child = create_product(structure="child", parent=self.prod)
self.category = catalogue_models.Category.add_root(name="root")
self.prod.categories.add(self.category)

def test_all_products_range(self):
self.assertTrue(self.range.contains_product(self.prod))
Expand All @@ -30,6 +33,15 @@ def test_blacklisting(self):
self.assertFalse(self.range.contains_product(self.prod))
self.assertNotIn(self.prod, self.range.all_products())

def test_category_blacklisting(self):
self.range.excluded_categories.add(self.category)
self.assertNotIn(self.range, models.Range.objects.contains_product(self.prod))
self.assertNotIn(self.range, models.Range.objects.contains_product(self.child))
self.assertFalse(self.range.contains_product(self.prod))
self.assertFalse(self.range.contains_product(self.child))
self.assertNotIn(self.prod, self.range.all_products())
self.assertNotIn(self.child, self.range.all_products())


class TestChildRange(TestCase):
def setUp(self):
Expand Down

0 comments on commit f09739a

Please sign in to comment.