# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2017, Shoop Commerce Ltd. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
import decimal
from collections import defaultdict
from itertools import chain
import six
from django import forms
from django.db.models import Q
from django.template.loader import render_to_string
from django.utils.safestring import mark_safe
from django.utils.text import capfirst, slugify
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import get_language
from shuup.core.models import (
Category, Manufacturer, ProductVariationVariable, ShopProduct,
ShopProductVisibility
)
from shuup.core.utils import context_cache
from shuup.front.utils.sorts_and_filters import (
get_configuration, ProductListFormModifier
)
from shuup.utils.i18n import format_money
[docs]class SimpleProductListModifier(ProductListFormModifier):
is_active_key = ""
is_active_label = ""
ordering_key = ""
ordering_label = ""
[docs] def should_use(self, configuration):
if not configuration:
return
return bool(configuration.get(self.is_active_key))
[docs] def get_ordering(self, configuration):
if not configuration:
return 1
return configuration.get(self.ordering_key, 1)
[docs] def get_admin_fields(self):
return [
(self.is_active_key, forms.BooleanField(label=self.is_active_label, required=False)),
(self.ordering_key, forms.IntegerField(label=self.ordering_label, initial=1))
]
[docs]class SortProductListByName(SimpleProductListModifier):
is_active_key = "sort_products_by_name"
is_active_label = _("Sort products by name")
ordering_key = "sort_products_by_name_ordering"
ordering_label = _("Ordering for sort by name")
[docs] def get_fields(self, request, category=None):
return [("sort", forms.CharField(required=False, widget=forms.Select(), label=_('Sort')))]
[docs] def get_choices_for_fields(self):
return [
("sort", [
("name_a", _("Name - A-Z")),
("name_d", _("Name - Z-A")),
]),
]
[docs] def sort_products(self, request, products, data):
sort = data.get("sort", "name_a")
def _get_product_name_lowered_stripped(product):
return product.name.lower().strip()
if not sort:
sort = ""
key = (sort[:-2] if sort.endswith(('_a', '_d')) else sort)
if key == "name":
sorter = _get_product_name_lowered_stripped
reverse = bool(sort.endswith('_d'))
products = sorted(products, key=sorter, reverse=reverse)
return products
[docs] def get_admin_fields(self):
default_fields = super(SortProductListByName, self).get_admin_fields()
default_fields[0][1].help_text = _(
"Check this to allow products to be sortable by product name."
)
default_fields[1][1].help_text = _(
"Use a numeric value to set the order in which the the filter will appear on the "
"product listing page."
)
return default_fields
[docs]class SortProductListByPrice(SimpleProductListModifier):
is_active_key = "sort_products_by_price"
is_active_label = _("Sort products by price")
ordering_key = "sort_products_by_price_ordering"
ordering_label = _("Ordering for sort by price")
[docs] def get_fields(self, request, category=None):
return [("sort", forms.CharField(required=False, widget=forms.Select(), label=_('Sort')))]
[docs] def get_choices_for_fields(self):
return [
("sort", [
("price_a", _("Price - Low to High")),
("price_d", _("Price - High to Low")),
]),
]
[docs] def sort_products(self, request, products, data):
sort = data.get("sort")
def _get_product_price_getter_for_request(request):
def _get_product_price(product):
return product.get_price(request)
return _get_product_price
if not sort:
sort = ""
key = (sort[:-2] if sort.endswith(('_a', '_d')) else sort)
if key == "price":
reverse = bool(sort.endswith('_d'))
sorter = _get_product_price_getter_for_request(request)
return sorted(products, key=sorter, reverse=reverse)
return products
[docs] def get_admin_fields(self):
default_fields = super(SortProductListByPrice, self).get_admin_fields()
default_fields[0][1].help_text = _(
"Check this to allow products to be sortable by price (from low to high and high to low)."
)
default_fields[1][1].help_text = _(
"Use a numeric value to set the order in which the the filter will appear on the "
"product listing page."
)
return default_fields
[docs]class SortProductListByCreatedDate(SimpleProductListModifier):
is_active_key = "sort_products_by_date_created"
is_active_label = _("Sort products by date created")
ordering_key = "sort_products_by_date_created_ordering"
ordering_label = _("Ordering for sort by date created")
[docs] def get_fields(self, request, category=None):
return [("sort", forms.CharField(required=False, widget=forms.Select(), label=_('Sort')))]
[docs] def get_choices_for_fields(self):
return [
("sort", [
("created_date_d", _("Date created")),
]),
]
[docs] def sort_products(self, request, products, data):
sort = data.get("sort")
def _get_product_created_on_datetime(product):
return product.created_on
if not sort:
sort = ""
key = (sort[:-2] if sort.endswith(('_a', '_d')) else sort)
if key == "created_date":
sorter = _get_product_created_on_datetime
reverse = bool(sort.endswith('_d'))
products = sorted(products, key=sorter, reverse=reverse)
return products
[docs] def get_admin_fields(self):
default_fields = super(SortProductListByCreatedDate, self).get_admin_fields()
default_fields[0][1].help_text = _(
"Check this to allow products to be sortable from newest to oldest products."
)
default_fields[1][1].help_text = _(
"Use a numeric value to set the order in which the filter will appear on the "
"product listing page."
)
return default_fields
[docs]class SortProductListByAscendingCreatedDate(SortProductListByCreatedDate):
is_active_key = "sort_products_by_ascending_created_date"
is_active_label = _("Sort products by date created - oldest first")
ordering_key = "sort_products_by_ascending_created_date_ordering"
ordering_label = _("Ordering for sort by date created - oldest first")
[docs] def get_choices_for_fields(self):
return [
("sort", [
("created_date_a", _("Date created - oldest first")),
]),
]
[docs] def get_admin_fields(self):
default_fields = super(SortProductListByAscendingCreatedDate, self).get_admin_fields()
default_fields[0][1].help_text = _(
"Check this to allow products to be sortable from oldest to newest products."
)
default_fields[1][1].help_text = _(
"Use a numeric value to set the order in which the filter will appear on the "
"product listing page."
)
return default_fields
[docs]class ManufacturerProductListFilter(SimpleProductListModifier):
is_active_key = "filter_products_by_manufacturer"
is_active_label = _("Filter products by manufacturer")
ordering_key = "filter_products_by_manufacturer_ordering"
ordering_label = _("Ordering for filter by manufacturer")
[docs] def get_fields(self, request, category=None):
if not Manufacturer.objects.exists():
return
if category:
manufacturer_ids = set(
ShopProduct.objects.filter(
categories__in=[category]).values_list("product__manufacturer__pk", flat=True).distinct()
)
if manufacturer_ids == set([None]):
return
queryset = Manufacturer.objects.filter(pk__in=manufacturer_ids)
else:
queryset = Manufacturer.objects.all()
return [
(
"manufacturers",
forms.ModelMultipleChoiceField(
queryset=queryset, required=False, label=_('Manufacturers'), widget=FilterWidget())
),
]
[docs] def get_filters(self, request, data):
manufacturers = data.get("manufacturers")
if manufacturers:
return Q(manufacturer__in=manufacturers)
[docs] def get_admin_fields(self):
default_fields = super(ManufacturerProductListFilter, self).get_admin_fields()
default_fields[0][1].help_text = _(
"Check this to allow products to be filterable by manufacturer for this category."
)
default_fields[1][1].help_text = _(
"Use a numeric value to set the order in which the manufacturer filters will appear on the "
"product listing page."
)
return default_fields
[docs]class CategoryProductListFilter(SimpleProductListModifier):
is_active_key = "filter_products_by_category"
is_active_label = _("Filter products by category")
ordering_key = "filter_products_by_category_ordering"
ordering_label = _("Ordering for filter by category")
[docs] def get_fields(self, request, category=None):
if not Category.objects.exists():
return
key, val = context_cache.get_cached_value(
identifier="categoryproductfilter", item=self, context=request, category=category)
if val:
return val
language = get_language()
base_queryset = Category.objects.all_visible(request.customer, request.shop, language=language)
if category:
q = Q(
Q(shop_products__categories=category),
~Q(shop_products__visibility=ShopProductVisibility.NOT_VISIBLE)
)
queryset = base_queryset.filter(q).exclude(pk=category.pk).distinct()
else:
# Show only first level when there is no category selected
queryset = base_queryset.filter(parent=None)
data = [
(
"categories",
forms.ModelMultipleChoiceField(
queryset=queryset, required=False, label=_('Categories'), widget=FilterWidget())
),
]
context_cache.set_cached_value(key, data)
return data
[docs] def get_filters(self, request, data):
categories = data.get("categories")
if categories:
return Q(shop_products__categories__in=list(categories))
[docs] def get_admin_fields(self):
default_fields = super(CategoryProductListFilter, self).get_admin_fields()
default_fields[0][1].help_text = _(
"Check this to allow products to be filterable by any visible product category. "
)
default_fields[1][1].help_text = _(
"Use a numeric value to set the order in which the category list filters will appear on the "
"product listing page."
)
return default_fields
[docs]class LimitProductListPageSize(SimpleProductListModifier):
is_active_key = "limit_product_list_page_size"
is_active_label = _("Limit page size")
ordering_key = "limit_product_list_page_size_ordering"
ordering_label = _("Ordering for limit page size")
[docs] def get_fields(self, request, category=None):
return [("limit", forms.IntegerField(required=False, widget=forms.Select(), label=_("Products per page")))]
[docs] def get_choices_for_fields(self):
return [
("limit", [(12, 12), (24, 24), (36, 36), (48, 48)]),
]
[docs] def get_admin_fields(self):
default_fields = super(LimitProductListPageSize, self).get_admin_fields()
default_fields[0][1].help_text = _(
"Check this to allow the customer to be able to select the number of products to display."
)
default_fields[1][1].help_text = _(
"Use a numeric value to set the order in which the page size filter will appear on the "
"product listing page."
)
return default_fields
[docs]class ProductVariationFilter(SimpleProductListModifier):
is_active_key = "filter_products_by_variation_value"
is_active_label = _("Filter products by variation")
ordering_key = "filter_products_by_variation_value_ordering"
ordering_label = _("Ordering for filter by variation")
[docs] def get_fields(self, request, category=None):
if not category:
return
key, val = context_cache.get_cached_value(
identifier="productvariationfilter", item=self, context=request, category=category)
if val:
return val
variation_values = defaultdict(set)
for variation in ProductVariationVariable.objects.filter(
Q(product__shop_products__categories=category),
~Q(product__shop_products__visibility=ShopProductVisibility.NOT_VISIBLE)):
for value in variation.values.all():
# TODO: Use ID here instead of this "trick"
choices = (value.value.replace(" ", "*"), value.value)
variation_values[slugify(variation.name)].add(choices)
fields = []
for variation_key, choices in six.iteritems(variation_values):
fields.append((
"variation_%s" % variation_key,
forms.MultipleChoiceField(
choices=choices, required=False, label=capfirst(variation_key), widget=FilterWidget())
))
context_cache.set_cached_value(key, fields)
return fields
[docs] def get_queryset(self, queryset, data):
if not any([key for key in data.keys() if key.startswith("variation")]):
return
for key, values in six.iteritems(data):
if key.startswith("variation"):
variation_query = Q()
for value in list(values):
# TODO: When using id this should search value for id
variation_query |= Q(
variation_variables__values__translations__value__iexact=value.replace("*", " "))
queryset = queryset.filter(variation_query)
return queryset
[docs] def get_admin_fields(self):
default_fields = super(ProductVariationFilter, self).get_admin_fields()
default_fields[0][1].help_text = _(
"Check this to allow products to be filterable by their different variations. "
"For example, size or color."
)
default_fields[1][1].help_text = _(
"Use a numeric value to set the order in which the variation filters will appear on the "
"product listing page."
)
return default_fields
[docs]class ProductPriceFilter(SimpleProductListModifier):
is_active_key = "filter_products_by_price"
is_active_label = _("Filter products by price")
ordering_key = "filter_products_by_price_ordering"
ordering_label = _("Ordering for filter by price")
range_min_key = "filter_products_by_price_range_min"
range_max_key = "filter_products_by_price_range_max"
range_size_key = "filter_products_by_price_range_size"
[docs] def get_fields(self, request, category=None):
if not category:
return
# TODO: Add cache
configuration = get_configuration(request.shop, category)
min_price = configuration.get(self.range_min_key)
max_price = configuration.get(self.range_max_key)
range_size = configuration.get(self.range_size_key)
if not (min_price and max_price and range_size):
return
choices = [(None, "-------")] + get_price_ranges(
request.shop, min_price, max_price, range_size)
return [
("price_range", forms.ChoiceField(required=False, choices=choices, label=_("Price"))),
]
[docs] def filter_products(self, request, products, data):
selected_range = data.get("price_range")
if not selected_range:
return products
min_price, max_price = selected_range.split("-", 1)
min_price_value = decimal.Decimal(min_price or 0)
max_price_value = decimal.Decimal(max_price or 0)
filtered_products = []
for product in products:
price_value = product.get_price(request).amount.value
if price_value >= min_price_value and (max_price == "" or price_value < max_price_value):
filtered_products.append(product)
return filtered_products
[docs] def get_admin_fields(self):
default_fields = super(ProductPriceFilter, self).get_admin_fields()
default_fields[0][1].help_text = _(
"Check this to allow products to be filtered by price. "
"Prices will be listed in groups from the price range minimum to price range maximum in increments of "
"the configured price range step."
)
default_fields[1][1].help_text = _(
"Use a numeric value to set the order in which the price range filters will appear on the "
"product listing page."
)
min_field = forms.IntegerField(
label=_("Price range minimum"), min_value=0, required=False,
help_text=_("Set the minimum price for the filter. The first range will be from zero to this value.")
)
max_field = forms.IntegerField(
label=_("Price range maximum"), min_value=0, required=False,
help_text=_("Set the maximum price for the filter. The last range will include this value and above.")
)
range_step = forms.IntegerField(
label=_("Price range step"), min_value=0, required=False,
help_text=_("Set the price step for each range. Each range will increment by this value.")
)
return default_fields + [
(self.range_min_key, min_field),
(self.range_max_key, max_field),
(self.range_size_key, range_step),
]
[docs]def get_price_ranges(shop, min_price, max_price, range_step):
if range_step == 0:
return
ranges = []
min_price_value = format_money(shop.create_price(min_price))
ranges.append(("-%s" % min_price, _("Under %(min_limit)s") % {"min_limit": min_price_value}))
for range_min in range(min_price, max_price, range_step):
range_min_price = format_money(shop.create_price(range_min))
range_max = range_min + range_step
if range_max < max_price:
range_max_price = format_money(shop.create_price(range_max))
ranges.append(
("%s-%s" % (range_min, range_max), "%s to %s" % (range_min_price, range_max_price)))
max_price_value = format_money(shop.create_price(max_price))
ranges.append(("%s-" % max_price, _("%(max_limit)s & Above") % {"max_limit": max_price_value}))
return ranges