diff --git a/cartridge/shop/defaults.py b/cartridge/shop/defaults.py index 31e20ef2c..955775366 100644 --- a/cartridge/shop/defaults.py +++ b/cartridge/shop/defaults.py @@ -293,3 +293,12 @@ editable=False, default=True, ) + +register_setting( + name="SHOP_USE_HIERARCHICAL_URLS", + label=_("Use hierarchical product URLs"), + description="If set and a product only belongs to a single category, " + "generate the product's URL in terms of that category.", + editable=False, + default=False, +) diff --git a/cartridge/shop/models.py b/cartridge/shop/models.py index e9e877821..eff7295ce 100644 --- a/cartridge/shop/models.py +++ b/cartridge/shop/models.py @@ -128,8 +128,25 @@ def save(self, *args, **kwargs): @models.permalink def get_absolute_url(self): + if settings.SHOP_USE_HIERARCHICAL_URLS: + category = self.get_category() + if category: + return ("shop_category_product", (), { + "category_slug": category.get_raw_slug(), + "slug": self.slug, + "product_id": self.id}) return ("shop_product", (), {"slug": self.slug}) + def get_category(self): + """ + Returns the single category this product is associated with, or None + if the number of categories is not exactly 1. + """ + categories = self.categories.all() + if len(categories) == 1: + return categories[0] + return None + def copy_default_variation(self): """ Copies the price and image fields from the default variation @@ -380,6 +397,14 @@ def filters(self): return reduce(operator, filters) return products + def get_raw_slug(self): + """ + Returns this object's slug stripped of its parent's slug. + """ + if not self.parent or not self.parent.slug: + return self.slug + return self.slug.lstrip(self.parent.slug).lstrip('/') + class Order(models.Model): diff --git a/cartridge/shop/urls.py b/cartridge/shop/urls.py index 2cb46ed5f..12fffab8c 100644 --- a/cartridge/shop/urls.py +++ b/cartridge/shop/urls.py @@ -8,4 +8,6 @@ url("^checkout/$", "checkout_steps", name="shop_checkout"), url("^checkout/complete/$", "complete", name="shop_complete"), url("^invoice/(?P\d+)/$", "invoice", name="shop_invoice"), + url("^(?P.+)/(?P.+)/(?P\d+)$", + "category_product", name="shop_category_product"), ) diff --git a/cartridge/shop/views.py b/cartridge/shop/views.py index 7926893fb..e9e8d18f3 100644 --- a/cartridge/shop/views.py +++ b/cartridge/shop/views.py @@ -31,13 +31,15 @@ order_handler = handler(settings.SHOP_HANDLER_ORDER) -def product(request, slug, template="shop/product.html"): +def product(request, slug, template="shop/product.html", product=None): """ Display a product - convert the product variations to JSON as well as handling adding the product to either the cart or the wishlist. """ - published_products = Product.objects.published(for_user=request.user) - product = get_object_or_404(published_products, slug=slug) + if not product: + published_products = Product.objects.published(for_user=request.user) + product = get_object_or_404(published_products, slug=slug) + fields = [f.name for f in ProductVariation.option_fields()] variations = product.variations.all() variations_json = simplejson.dumps([dict([(f, getattr(v, f)) @@ -81,6 +83,25 @@ def product(request, slug, template="shop/product.html"): } return render(request, template, context) +def category_product(request, category_slug, slug, product_id, + template="shop/product.html"): + """ + Display a product in terms of its category's slug. Wrapper around + product(). Only enabled when SHOP_USE_HIERARCHICAL_URLS is True. + """ + published_products = Product.objects.published(for_user=request.user) + product_obj = get_object_or_404(published_products, id=product_id) + category = product_obj.get_category() + + # Tolerate stale URLs by redirecting to the new URL. + if not settings.SHOP_USE_HIERARCHICAL_URLS or not category: + # Setting is disabled or category has been removed. + return redirect(product_obj.get_absolute_url(), permanent=True) + elif slug != product_obj.slug or category_slug != category.get_raw_slug(): + # Category or product slug mismatch. + return redirect(product_obj.get_absolute_url(), permanent=True) + + return product(request, slug, template=template, product=product_obj) @never_cache def wishlist(request, template="shop/wishlist.html"):