import warnings
from django.contrib import messages
from django.core.paginator import InvalidPage
from django.http import Http404, HttpResponsePermanentRedirect
from django.shortcuts import get_object_or_404, redirect
from django.utils.http import urlquote
from django.utils.translation import ugettext_lazy as _
from django.views.generic import DetailView, TemplateView
from oscar.apps.catalogue.signals import product_viewed
from oscar.core.loading import get_class, get_model
Product = get_model('catalogue', 'product')
Category = get_model('catalogue', 'category')
ProductAlert = get_model('customer', 'ProductAlert')
ProductAlertForm = get_class('customer.forms', 'ProductAlertForm')
get_product_search_handler_class = get_class(
'catalogue.search_handlers', 'get_product_search_handler_class')
class ProductDetailView(DetailView):
context_object_name = 'product'
model = Product
view_signal = product_viewed
template_folder = "catalogue"
# Whether to redirect to the URL with the right path
enforce_paths = True
# Whether to redirect child products to their parent's URL
enforce_parent = True
def get(self, request, **kwargs):
"""
Ensures that the correct URL is used before rendering a response
"""
self.object = product = self.get_object()
redirect = self.redirect_if_necessary(request.path, product)
if redirect is not None:
return redirect
response = super(ProductDetailView, self).get(request, **kwargs)
self.send_signal(request, response, product)
return response
def get_object(self, queryset=None):
# Check if self.object is already set to prevent unnecessary DB calls
if hasattr(self, 'object'):
return self.object
else:
return super(ProductDetailView, self).get_object(queryset)
def redirect_if_necessary(self, current_path, product):
if self.enforce_parent and product.is_child:
return HttpResponsePermanentRedirect(
product.parent.get_absolute_url())
if self.enforce_paths:
expected_path = product.get_absolute_url()
if expected_path != urlquote(current_path):
return HttpResponsePermanentRedirect(expected_path)
def get_context_data(self, **kwargs):
ctx = super(ProductDetailView, self).get_context_data(**kwargs)
ctx['alert_form'] = self.get_alert_form()
ctx['has_active_alert'] = self.get_alert_status()
return ctx
def get_alert_status(self):
# Check if this user already have an alert for this product
has_alert = False
if self.request.user.is_authenticated():
alerts = ProductAlert.objects.filter(
product=self.object, user=self.request.user,
status=ProductAlert.ACTIVE)
has_alert = alerts.exists()
return has_alert
def get_alert_form(self):
return ProductAlertForm(
user=self.request.user, product=self.object)
def send_signal(self, request, response, product):
self.view_signal.send(
sender=self, product=product, user=request.user, request=request,
response=response)
def get_template_names(self):
"""
Return a list of possible templates.
If an overriding class sets a template name, we use that. Otherwise,
we try 2 options before defaulting to catalogue/detail.html:
1). detail-for-upc-<upc>.html
2). detail-for-class-<classname>.html
This allows alternative templates to be provided for a per-product
and a per-item-class basis.
"""
if self.template_name:
return [self.template_name]
return [
'%s/detail-for-upc-%s.html' % (
self.template_folder, self.object.upc),
'%s/detail-for-class-%s.html' % (
self.template_folder, self.object.get_product_class().slug),
'%s/detail.html' % (self.template_folder)]
[docs]class CatalogueView(TemplateView):
"""
Browse all products in the catalogue
"""
context_object_name = "products"
template_name = 'catalogue/browse.html'
def get(self, request, *args, **kwargs):
try:
self.search_handler = self.get_search_handler(
self.request.GET, request.get_full_path(), [])
except InvalidPage:
# Redirect to page one.
messages.error(request, _('The given page number was invalid.'))
return redirect('catalogue:index')
return super(CatalogueView, self).get(request, *args, **kwargs)
def get_search_handler(self, *args, **kwargs):
return get_product_search_handler_class()(*args, **kwargs)
def get_context_data(self, **kwargs):
ctx = {}
ctx['summary'] = _("All products")
search_context = self.search_handler.get_search_context_data(
self.context_object_name)
ctx.update(search_context)
return ctx
[docs]class ProductCategoryView(TemplateView):
"""
Browse products in a given category
"""
context_object_name = "products"
template_name = 'catalogue/category.html'
enforce_paths = True
def get(self, request, *args, **kwargs):
# Fetch the category; return 404 or redirect as needed
self.category = self.get_category()
potential_redirect = self.redirect_if_necessary(
request.path, self.category)
if potential_redirect is not None:
return potential_redirect
try:
self.search_handler = self.get_search_handler(
request.GET, request.get_full_path(), self.get_categories())
except InvalidPage:
messages.error(request, _('The given page number was invalid.'))
return redirect(self.category.get_absolute_url())
return super(ProductCategoryView, self).get(request, *args, **kwargs)
def get_category(self):
if 'pk' in self.kwargs:
# Usual way to reach a category page. We just look at the primary
# key, which is easy on the database. If the slug changed, get()
# will redirect appropriately.
# WARNING: Category.get_absolute_url needs to look up it's parents
# to compute the URL. As this is slightly expensive, Oscar's
# default implementation caches the method. That's pretty safe
# as ProductCategoryView does the lookup by primary key, which
# will work even if the cache is stale. But if you override this
# logic, consider if that still holds true.
return get_object_or_404(Category, pk=self.kwargs['pk'])
elif 'category_slug' in self.kwargs:
# DEPRECATED. TODO: Remove in Oscar 1.2.
# For SEO and legacy reasons, we allow chopping off the primary
# key from the URL. In that case, we have the target category slug
# and it's ancestors' slugs concatenated together.
# To save on queries, we pick the last slug, look up all matching
# categories and only then compare.
# Note that currently we enforce uniqueness of slugs, but as that
# might feasibly change soon, it makes sense to be forgiving here.
concatenated_slugs = self.kwargs['category_slug']
slugs = concatenated_slugs.split(Category._slug_separator)
try:
last_slug = slugs[-1]
except IndexError:
raise Http404
else:
for category in Category.objects.filter(slug=last_slug):
if category.full_slug == concatenated_slugs:
message = (
"Accessing categories without a primary key"
" is deprecated will be removed in Oscar 1.2.")
warnings.warn(message, DeprecationWarning)
return category
raise Http404
def redirect_if_necessary(self, current_path, category):
if self.enforce_paths:
# Categories are fetched by primary key to allow slug changes.
# If the slug has changed, issue a redirect.
expected_path = category.get_absolute_url()
if expected_path != urlquote(current_path):
return HttpResponsePermanentRedirect(expected_path)
def get_search_handler(self, *args, **kwargs):
return get_product_search_handler_class()(*args, **kwargs)
[docs] def get_categories(self):
"""
Return a list of the current category and its ancestors
"""
return self.category.get_descendants_and_self()
def get_context_data(self, **kwargs):
context = super(ProductCategoryView, self).get_context_data(**kwargs)
context['category'] = self.category
search_context = self.search_handler.get_search_context_data(
self.context_object_name)
context.update(search_context)
return context