需要有关如何制作价格范围过滤器(价格滑块)的正确方法的建议
Need advice about a proper way how to make a price range filter (price slider)
Oscar
有这样的facet配置结构:
OSCAR_SEARCH_FACETS = {
'fields': {
'rating': {
'name': _('Rating'),
'field': 'rating',
'options': {'sort': 'index'}
},
'vendor': {
'name': _('Vendor'),
'field': 'vendor',
},
}
'queries': {
'price_range': {
'name': _('Price range'),
'field': 'price',
'queries': [
(_('0 to 1000'), u'[0 TO 1000]'),
(_('1000 to 2000'), u'[1000 TO 2000]'),
(_('2000 to 4000'), u'[2000 TO 4000]'),
(_('4000+'), u'[4000 TO *]'),
]
},
}
}
queries
是 'static',我想使其动态依赖于类别内产品的价格。
基于 OSCAR_SEARCH_FACETS
,Oscar 使用 the next code
# oscar/apps/search/search_handlers.py
class SearchHandler(object)::
# some other methods
def get_search_context_data(self, context_object_name=None):
# all comments are removed. See source link above.
munger = self.get_facet_munger()
facet_data = munger.facet_data()
has_facets = any([data['results'] for data in facet_data.values()])
context = {
'facet_data': facet_data,
'has_facets': has_facets,
'selected_facets': self.request_data.getlist('selected_facets'),
'form': self.search_form,
'paginator': self.paginator,
'page_obj': self.page,
}
if context_object_name is not None:
context[context_object_name] = self.get_paginated_objects()
return context
生成下一个context
:
{'facet_data': {
'rating': {
'name': 'Рейтинг',
'results': [{'name': '5', 'count': 1, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=rating_exact%3A5'}]},
'vendor': {
'name': 'Vendor',
'results': [
{'name': 'AMD', 'count': 103, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=vendor_exact%3AAMD'},
{'name': 'INTEL', 'count': 119, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=vendor_exact%3AINTEL'}]},
'price_range': {
'name': 'Price Range',
'results': [
{'name': 'from 0 to 1000', 'count': 14, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=price_exact%3A%5B0+TO+1000%5D'},
{'name': 'from 1000 to 20000', 'count': 55, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=price_exact%3A%5B1000+TO+2000%5D'},
{'name': 'from 2000 to 4000', 'count': 66, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=price_exact%3A%5B2000+TO+4000%5D'},
{'name': 'более 4000', 'count': 89, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=price_exact%3A%5B4000+TO+%2A%5D'}]},
'has_facets': True, 'selected_facets': [], 'form': <BrowseCategoryForm bound=True, valid=True, fields=(q;sort_by)>, 'paginator': <django.core.paginator.Paginator object at 0x7f4c904c4d68>, 'page_obj': <Page 10 of 10>}}
我可以替换生成的 price_range
数据,如下所示:
facet_data['price_range']['results'] = [dict(min_price=SOME_MIN_PRICE, max_price=SOME_MAX_PRICE)]
我知道如何获得 SOME_MIN_PRICE
和 SOME_MAX_PRICE
,但我在 url
上遇到问题,它过滤产品 -> 我找不到方法,如何我可以为这个动态方面生成一个有效的 url。
例如,如果我在浏览器中手动更改范围(例如在查询 ?selected_facets=price_exact%3A%5B0+TO+1000%5D
中我将 1000 更改为 1001),Oscar returns 我所在类别的所有产品。
谁能告诉我url的解决方案,如果总体上有更好的方法,请指出方向?
首先我想说这个方法很脏,特别是在js中需要准备URL才能申请价格范围的部分。如果有人知道或希望通过 Oscar\Haystack 代码实现可行的 URLs - 欢迎。
小记:不知道是Oscar设计的还是我现在项目的前任dev决定的,我的模型有下一个结构
from oscar.apps.catalogue.abstract_models import AbstractProduct
class Product(AbstractProduct):
short_description = models.TextField(_('Short description'), blank=True)
def get_build_absolute_url(self):
...
def cache_delete(self, computers):
...
def save(self, *args, **kwargs):
...
class CPU(Product):
class Meta:
verbose_name = _('Processor')
verbose_name_plural = _('Processors')
class Cooler(Product):
class Meta:
verbose_name = _('Cooler')
verbose_name_plural = _('Coolers')
etc...
在我的例子中,我有一个前端目录,其中包含与模型相关的类别,即一个 Django 模型,例如 CPU 模型有一个前端产品类别,仅包含 CPUs。不得在一个类别中混用不同类型的产品。
基于这个模型结构,很难找出客户属于哪个类别,因为 search_handlers.py
下面 returns Product 实例的 self.categories[0].product_set.first()
不合适,因为我需要实例CPU、Cooler 等,以定义客户所在类别的 min\max 价格。
开始吧
阅读代码内的注释了解详情。
某处(大概base.html
)掉落:
<script type="text/JavaScript" src="{% static 'your_project/js/credit.min_0s.js' %}"></script>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
OSCAR_SEARCH_FACETS
应该是什么样子:
OSCAR_SEARCH_FACETS = {
'fields': OrderedDict([
....
]),
# WHAT WE NEED HERE: 'queries' -> 'price_range'
'queries': OrderedDict([
('price_range',
{
'name': _('Price range'),
'field': 'price',
'queries': [
(_('0 to *'), u'[0 TO *]') # Content of this does not matter
]
}),
]),
....
# For my possible future needs I added the line below which currently produce ['price_exact']
# If you do not need it, replace everywhere in the code "settings.OSCAR_SEARCH_FACETS['dynamic_queries_field_names']" to ['price_exact']
# If you want to have just str 'price_exact' (no list), doublecheck JS code "if (dynamic_query_fields.indexOf(k) >= 0)"
'dynamic_queries_field_names': [field + '_exact' for field in ('price', )]
}
创建 \search\search_handlers.py
和 \search\forms.py
以覆盖 Oscar 文件。在哪里创建?如果您不知道,那么可能在您的 'project' 文件夹中,即。在您的 'some_app' 文件夹旁边。
在search_handlers.py
中添加:
import json
from django.conf import settings
from haystack.query import SearchQuerySet
from oscar.core.loading import get_model
from oscar.apps.search.search_handlers import *
class SearchHandler(SearchHandler):
def get_search_context_data(self, context_object_name=None):
"""
Return metadata about the search in a dictionary useful to populate
template contexts. If you pass in a context_object_name, the dictionary
will also contain the actual list of found objects.
The expected usage is to call this function in your view's
get_context_data:
search_context = self.search_handler.get_search_context_data(
self.context_object_name)
context.update(search_context)
return context
"""
# Use the FacetMunger to convert Haystack's awkward facet data into
# something the templates can use.
# Note that the FacetMunger accesses object_list (unpaginated results),
# whereas we use the paginated search results to populate the context
# with products
munger = self.get_facet_munger()
facet_data = munger.facet_data()
has_facets = any([data['results'] for data in facet_data.values()])
# ADDED PART
# self.results sometimes returns category min\max price and sometimes according to filter min\max price, so
# the behaviour is not stable
# price_stats = self.results.stats('price').stats_results()['price']
# So, stable approach:
# Get a first product from Front-End category, i.e Hardware -> CPUs
product_id_from_current_category = self.categories[0].product_set.first().pk
from catalogue.models import Product # needs to populate vars()['Product']. Do not move to top - will not work.
child_models = [cls.__name__ for cls in vars()['Product'].__subclasses__()]
for model_name in child_models:
ChildModel = get_model('catalogue', model_name)
if ChildModel.objects.filter(pk=product_id_from_current_category).exists():
break
price_stats = SearchQuerySet().models(ChildModel).stats('price').stats_results()['price']
min_category_price, max_category_price = round(price_stats['min']), round(price_stats['max'])
dynamic_query_fields = json.dumps(settings.OSCAR_SEARCH_FACETS['dynamic_queries_field_names'])
facet_data['price_range']['results'] = dict(min_category_price=min_category_price,
max_category_price=max_category_price,
dynamic_query_fields=dynamic_query_fields)
# END
context = {
'facet_data': facet_data,
'has_facets': has_facets,
# This is a serious code smell; we just pass through the selected
# facets data to the view again, and the template adds those
# as fields to the form. This hack ensures that facets stay
# selected when changing relevancy.
'selected_facets': self.request_data.getlist('selected_facets'),
'form': self.search_form,
'paginator': self.paginator,
'page_obj': self.page,
}
# It's a pretty common pattern to want the actual results in the
# context, so pass them in if context_object_name is set.
if context_object_name is not None:
context[context_object_name] = self.get_paginated_objects()
return context
在forms.py
中:
from collections import defaultdict
from django import forms
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from haystack.forms import FacetedSearchForm
from oscar.apps.search.forms import SearchInput
from oscar.core.loading import get_class
is_solr_supported = get_class('search.features', 'is_solr_supported')
# Build a dict of valid queries
VALID_FACET_QUERIES = defaultdict(list)
for facet in settings.OSCAR_SEARCH_FACETS['queries'].values():
field_name = "%s_exact" % facet['field']
queries = [t[1] for t in facet['queries']]
VALID_FACET_QUERIES[field_name].extend(queries)
class SearchForm(FacetedSearchForm):
"""
In Haystack, the search form is used for interpreting
and sub-filtering the SQS.
"""
# Use a tabindex of 1 so that users can hit tab on any page and it will
# focus on the search widget.
q = forms.CharField(
required=False, label=_('Search'),
widget=SearchInput({
"placeholder": _('Search'),
"tabindex": "1",
"class": "form-control"
}))
# Search
RELEVANCY = "relevancy"
TOP_RATED = "rating"
NEWEST = "newest"
PRICE_HIGH_TO_LOW = "price-desc"
PRICE_LOW_TO_HIGH = "price-asc"
TITLE_A_TO_Z = "title-asc"
TITLE_Z_TO_A = "title-desc"
SORT_BY_CHOICES = [
(PRICE_LOW_TO_HIGH, _("Price low to high")),
(PRICE_HIGH_TO_LOW, _("Price high to low")),
(NEWEST, _("Newest")),
(TOP_RATED, _("Customer rating")),
]
# Map query params to sorting fields. Note relevancy isn't included here
# as we assume results are returned in relevancy order in the absence of an
# explicit sort field being passed to the search backend.
SORT_BY_MAP = {
TOP_RATED: '-rating',
NEWEST: '-date_created',
PRICE_HIGH_TO_LOW: '-price',
PRICE_LOW_TO_HIGH: 'price',
TITLE_A_TO_Z: 'title_s',
TITLE_Z_TO_A: '-title_s',
}
# Non Solr backends don't support dynamic fields so we just sort on title
if not is_solr_supported():
SORT_BY_MAP[TITLE_A_TO_Z] = 'title'
SORT_BY_MAP[TITLE_Z_TO_A] = '-title'
sort_by = forms.ChoiceField(
label=_("Sort by"), choices=SORT_BY_CHOICES,
widget=forms.Select(), required=False)
# Implementation of Price range filter based on:
# https://github.com/django-oscar/django-oscar/blob/master/src/oscar/apps/search/forms.py#L86
@property
def selected_multi_facets(self):
"""
Validate and return the selected facets
"""
# Process selected facets into a dict(field->[*values]) to handle
# multi-faceting
selected_multi_facets = defaultdict(list)
for facet_kv in self.selected_facets:
if ":" not in facet_kv:
continue
field_name, value = facet_kv.split(':', 1)
# EDITED PART comparing to original Oscar source
# Validate query facets as they as passed unescaped to Solr
if field_name in VALID_FACET_QUERIES:
if field_name in settings.OSCAR_SEARCH_FACETS['dynamic_queries_field_names']:
pass
else:
if value not in VALID_FACET_QUERIES[field_name]:
# Invalid query value
continue
# END
selected_multi_facets[field_name].append(value)
return selected_multi_facets
static/js/price_range_filter.js
看起来像:
$(document).ready(function() {
// Next vars are included in price_range_filter.html, as we need to provide data from that template to this js.
// var min_category_price = Number("{{ facet_data.price_range.results.min_category_price }}".replace(/\s/g,'')),
// max_category_price = Number("{{ facet_data.price_range.results.max_category_price }}".replace(/\s/g,'')),
// dynamic_query_fields = JSON.parse("{{ facet_data.price_range.results.dynamic_query_fields|escapejs }}"),
// current_url = "{{ request.get_full_path }}";
var category_url = current_url.split('/?selected_facets')[0],
min_filtered_price = 0,
max_filtered_price = 0;
// 1. Extracts queries (as key:value) from URL
// 2. Applies price range to Input Fields and Slider
// 3. Rebuilds 'submit' URL of price range
function handleUrl(use_globals_filtered_prices) {
//
var qd = {},
base_url_part = 'selected_facets=',
rebuilt_url ='?';
if (location.search) location.search.substr(1).split("&").forEach(function(item) {
var s = item.split("="),
k = s[0],
v = s[1] && decodeURIComponent(s[1]); // null-coalescing / short-circuit
//(k in qd) ? qd[k].push(v) : qd[k] = [v]
(qd[k] = qd[k] || []).push(v) // null-coalescing / short-circuit
});
// End of Whosebug
var facets = qd['selected_facets'],
price_changed = false;
for (var i in facets) {
var kv = facets[i],
k = kv.split(':')[0], // price_exact
v = kv.split(':')[1]; // [8732+TO+54432]
// Get filtered price range from URL and set Input Fields and Slider according to this range
// If k in dynamic_query_fields
if (dynamic_query_fields.indexOf(k) >= 0) {
// Replace existing price range in URL. Used when price range is changed
if (use_globals_filtered_prices){
kv = k + ':' + '[' + min_filtered_price + '+TO+' + max_filtered_price + ']';
price_changed = true;
}
// Just get min\max_filtered_prices and apply to Input Fields and Slider. Used when page is load
else {
min_filtered_price = v.substring(v.lastIndexOf("[")+1, v.lastIndexOf("+TO"));
max_filtered_price = v.substring(v.lastIndexOf("+TO+")+4, v.lastIndexOf("]"));
$('input.sliderValue[data-index="0"]').val(min_filtered_price);
$('input.sliderValue[data-index="1"]').val(max_filtered_price);
// 0 and 1 are field indexes
$("#slider").slider("values", 0, min_filtered_price);
$("#slider").slider("values", 1, max_filtered_price);
}
}
rebuilt_url += base_url_part + kv + '&';
}
// When we set price range at the first time, i.e when there is no previous version of price range facet.
if (use_globals_filtered_prices && !price_changed) {
kv = base_url_part + 'price_exact' + ':' + '[' + min_filtered_price + '+TO+' + max_filtered_price + ']';
rebuilt_url += kv;
}
if (rebuilt_url.slice(-1) === '&') {
rebuilt_url = rebuilt_url.slice(0, -1);
}
// If facets not selected
if (rebuilt_url !== '?') {
var full_url = category_url + encodeURI(rebuilt_url).replace(/:\s*/g, "%3A");
$("#submit_price").attr("href", full_url);
}
}
// SLIDER
$("#slider").slider({
min: min_category_price,
max: max_category_price,
step: 100,
range: true,
values: [min_category_price, max_category_price],
// After sliders are moved, change Input Field Values
slide: function(event, ui) {
for (var i = 0; i < ui.values.length; ++i) {
$("input.sliderValue[data-index=" + i + "]").val(ui.values[i]);
if (i === 0){
min_filtered_price = ui.values[i];
}
else {
max_filtered_price = ui.values[i]
}
handleUrl(true);
}
}
});
// INPUT FIELDS
$("input.sliderValue").change(function() {
var $this = $(this),
changed_field = $this.data("index"),
changed_price = $this.val();
$("#slider").slider("values", changed_field, changed_price);
if (changed_field === 0){
min_filtered_price = changed_price;
//Fix "0" max range URL price when just min range is changed
if (max_filtered_price === 0){
max_filtered_price = max_category_price;
}
}
else {
//Fix "0" min range URL price when just max range is changed
if (min_filtered_price === 0){
min_filtered_price = min_category_price;
}
max_filtered_price = changed_price;
}
handleUrl(true);
});
// # Executes once the page is loaded
handleUrl(false);
});
facets template
扩展了 category template
(客户看到产品的地方)并包含 price range filter
的 html 代码:
{% extends "catalogue/category.html" %}
{% block category_facets %}
{% if facet_data.price_range.results %}
{% include 'search/partials/price_range_filter.html' %}
{% endif %}
{% with facet_data.vendor as data %}
{% if data.results %}
{% include 'search/partials/facet.html' with name=data.name items=data.results %}
{% endif %}
{% endwith %}
{# OTHET FACETS #}
{% endblock %}
创建root/templates/search/partials/price_range_filter.html
。这看起来像 Oscar 的结构,但不会覆盖任何内容,因为 Oscar 没有 price_range_filter.html
。我决定在这里删除 price_range_filter.html
因为 Oscar 通常负责过滤器。
price_range_filter.html
看起来像(将样式放入 css,如果你愿意的话:)):
{% load staticfiles %}
<dl>
<dt class="nav-header">{{ facet_data.price_range.name }}</dt>
<div style="display: flex;">
<input type="text" class="sliderValue" data-index="0"
value="{{ facet_data.price_range.results.min_category_price }}"
style="width: 70px; margin-right: 10px"/>
<input type="text" class="sliderValue" data-index="1"
value="{{ facet_data.price_range.results.max_category_price }}"
style="width: 70px; margin-right: 10px"/>
<a id="submit_price" href="" class="btn btn-default">OK</a>
</div>
<br />
<div id="slider"></div>
</dl>
{% block extrascripts %}
<script>
var min_category_price = Number("{{ facet_data.price_range.results.min_category_price }}".replace(/\s/g,'')),
max_category_price = Number("{{ facet_data.price_range.results.max_category_price }}".replace(/\s/g,'')),
dynamic_query_fields = JSON.parse("{{ facet_data.price_range.results.dynamic_query_fields|escapejs }}"),
current_url = "{{ request.get_full_path }}";
</script>
<script type="text/JavaScript" src="{% static 'js/price_range_filter.js' %}"></script>
{% endblock %}
我不是 'pro' 程序员,所以欢迎 advices\improvements :)
奖金:
Oscar
有这样的facet配置结构:
OSCAR_SEARCH_FACETS = {
'fields': {
'rating': {
'name': _('Rating'),
'field': 'rating',
'options': {'sort': 'index'}
},
'vendor': {
'name': _('Vendor'),
'field': 'vendor',
},
}
'queries': {
'price_range': {
'name': _('Price range'),
'field': 'price',
'queries': [
(_('0 to 1000'), u'[0 TO 1000]'),
(_('1000 to 2000'), u'[1000 TO 2000]'),
(_('2000 to 4000'), u'[2000 TO 4000]'),
(_('4000+'), u'[4000 TO *]'),
]
},
}
}
queries
是 'static',我想使其动态依赖于类别内产品的价格。
基于 OSCAR_SEARCH_FACETS
,Oscar 使用 the next code
# oscar/apps/search/search_handlers.py
class SearchHandler(object)::
# some other methods
def get_search_context_data(self, context_object_name=None):
# all comments are removed. See source link above.
munger = self.get_facet_munger()
facet_data = munger.facet_data()
has_facets = any([data['results'] for data in facet_data.values()])
context = {
'facet_data': facet_data,
'has_facets': has_facets,
'selected_facets': self.request_data.getlist('selected_facets'),
'form': self.search_form,
'paginator': self.paginator,
'page_obj': self.page,
}
if context_object_name is not None:
context[context_object_name] = self.get_paginated_objects()
return context
生成下一个context
:
{'facet_data': {
'rating': {
'name': 'Рейтинг',
'results': [{'name': '5', 'count': 1, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=rating_exact%3A5'}]},
'vendor': {
'name': 'Vendor',
'results': [
{'name': 'AMD', 'count': 103, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=vendor_exact%3AAMD'},
{'name': 'INTEL', 'count': 119, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=vendor_exact%3AINTEL'}]},
'price_range': {
'name': 'Price Range',
'results': [
{'name': 'from 0 to 1000', 'count': 14, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=price_exact%3A%5B0+TO+1000%5D'},
{'name': 'from 1000 to 20000', 'count': 55, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=price_exact%3A%5B1000+TO+2000%5D'},
{'name': 'from 2000 to 4000', 'count': 66, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=price_exact%3A%5B2000+TO+4000%5D'},
{'name': 'более 4000', 'count': 89, 'show_count': True, 'disabled': False, 'selected': False, 'select_url': '/catalogue/category/hardware/cpu_2/?selected_facets=price_exact%3A%5B4000+TO+%2A%5D'}]},
'has_facets': True, 'selected_facets': [], 'form': <BrowseCategoryForm bound=True, valid=True, fields=(q;sort_by)>, 'paginator': <django.core.paginator.Paginator object at 0x7f4c904c4d68>, 'page_obj': <Page 10 of 10>}}
我可以替换生成的 price_range
数据,如下所示:
facet_data['price_range']['results'] = [dict(min_price=SOME_MIN_PRICE, max_price=SOME_MAX_PRICE)]
我知道如何获得 SOME_MIN_PRICE
和 SOME_MAX_PRICE
,但我在 url
上遇到问题,它过滤产品 -> 我找不到方法,如何我可以为这个动态方面生成一个有效的 url。
例如,如果我在浏览器中手动更改范围(例如在查询 ?selected_facets=price_exact%3A%5B0+TO+1000%5D
中我将 1000 更改为 1001),Oscar returns 我所在类别的所有产品。
谁能告诉我url的解决方案,如果总体上有更好的方法,请指出方向?
首先我想说这个方法很脏,特别是在js中需要准备URL才能申请价格范围的部分。如果有人知道或希望通过 Oscar\Haystack 代码实现可行的 URLs - 欢迎。
小记:不知道是Oscar设计的还是我现在项目的前任dev决定的,我的模型有下一个结构
from oscar.apps.catalogue.abstract_models import AbstractProduct
class Product(AbstractProduct):
short_description = models.TextField(_('Short description'), blank=True)
def get_build_absolute_url(self):
...
def cache_delete(self, computers):
...
def save(self, *args, **kwargs):
...
class CPU(Product):
class Meta:
verbose_name = _('Processor')
verbose_name_plural = _('Processors')
class Cooler(Product):
class Meta:
verbose_name = _('Cooler')
verbose_name_plural = _('Coolers')
etc...
在我的例子中,我有一个前端目录,其中包含与模型相关的类别,即一个 Django 模型,例如 CPU 模型有一个前端产品类别,仅包含 CPUs。不得在一个类别中混用不同类型的产品。
基于这个模型结构,很难找出客户属于哪个类别,因为 search_handlers.py
下面 returns Product 实例的 self.categories[0].product_set.first()
不合适,因为我需要实例CPU、Cooler 等,以定义客户所在类别的 min\max 价格。
开始吧
阅读代码内的注释了解详情。
某处(大概base.html
)掉落:
<script type="text/JavaScript" src="{% static 'your_project/js/credit.min_0s.js' %}"></script>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
OSCAR_SEARCH_FACETS
应该是什么样子:
OSCAR_SEARCH_FACETS = {
'fields': OrderedDict([
....
]),
# WHAT WE NEED HERE: 'queries' -> 'price_range'
'queries': OrderedDict([
('price_range',
{
'name': _('Price range'),
'field': 'price',
'queries': [
(_('0 to *'), u'[0 TO *]') # Content of this does not matter
]
}),
]),
....
# For my possible future needs I added the line below which currently produce ['price_exact']
# If you do not need it, replace everywhere in the code "settings.OSCAR_SEARCH_FACETS['dynamic_queries_field_names']" to ['price_exact']
# If you want to have just str 'price_exact' (no list), doublecheck JS code "if (dynamic_query_fields.indexOf(k) >= 0)"
'dynamic_queries_field_names': [field + '_exact' for field in ('price', )]
}
创建 \search\search_handlers.py
和 \search\forms.py
以覆盖 Oscar 文件。在哪里创建?如果您不知道,那么可能在您的 'project' 文件夹中,即。在您的 'some_app' 文件夹旁边。
在search_handlers.py
中添加:
import json
from django.conf import settings
from haystack.query import SearchQuerySet
from oscar.core.loading import get_model
from oscar.apps.search.search_handlers import *
class SearchHandler(SearchHandler):
def get_search_context_data(self, context_object_name=None):
"""
Return metadata about the search in a dictionary useful to populate
template contexts. If you pass in a context_object_name, the dictionary
will also contain the actual list of found objects.
The expected usage is to call this function in your view's
get_context_data:
search_context = self.search_handler.get_search_context_data(
self.context_object_name)
context.update(search_context)
return context
"""
# Use the FacetMunger to convert Haystack's awkward facet data into
# something the templates can use.
# Note that the FacetMunger accesses object_list (unpaginated results),
# whereas we use the paginated search results to populate the context
# with products
munger = self.get_facet_munger()
facet_data = munger.facet_data()
has_facets = any([data['results'] for data in facet_data.values()])
# ADDED PART
# self.results sometimes returns category min\max price and sometimes according to filter min\max price, so
# the behaviour is not stable
# price_stats = self.results.stats('price').stats_results()['price']
# So, stable approach:
# Get a first product from Front-End category, i.e Hardware -> CPUs
product_id_from_current_category = self.categories[0].product_set.first().pk
from catalogue.models import Product # needs to populate vars()['Product']. Do not move to top - will not work.
child_models = [cls.__name__ for cls in vars()['Product'].__subclasses__()]
for model_name in child_models:
ChildModel = get_model('catalogue', model_name)
if ChildModel.objects.filter(pk=product_id_from_current_category).exists():
break
price_stats = SearchQuerySet().models(ChildModel).stats('price').stats_results()['price']
min_category_price, max_category_price = round(price_stats['min']), round(price_stats['max'])
dynamic_query_fields = json.dumps(settings.OSCAR_SEARCH_FACETS['dynamic_queries_field_names'])
facet_data['price_range']['results'] = dict(min_category_price=min_category_price,
max_category_price=max_category_price,
dynamic_query_fields=dynamic_query_fields)
# END
context = {
'facet_data': facet_data,
'has_facets': has_facets,
# This is a serious code smell; we just pass through the selected
# facets data to the view again, and the template adds those
# as fields to the form. This hack ensures that facets stay
# selected when changing relevancy.
'selected_facets': self.request_data.getlist('selected_facets'),
'form': self.search_form,
'paginator': self.paginator,
'page_obj': self.page,
}
# It's a pretty common pattern to want the actual results in the
# context, so pass them in if context_object_name is set.
if context_object_name is not None:
context[context_object_name] = self.get_paginated_objects()
return context
在forms.py
中:
from collections import defaultdict
from django import forms
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from haystack.forms import FacetedSearchForm
from oscar.apps.search.forms import SearchInput
from oscar.core.loading import get_class
is_solr_supported = get_class('search.features', 'is_solr_supported')
# Build a dict of valid queries
VALID_FACET_QUERIES = defaultdict(list)
for facet in settings.OSCAR_SEARCH_FACETS['queries'].values():
field_name = "%s_exact" % facet['field']
queries = [t[1] for t in facet['queries']]
VALID_FACET_QUERIES[field_name].extend(queries)
class SearchForm(FacetedSearchForm):
"""
In Haystack, the search form is used for interpreting
and sub-filtering the SQS.
"""
# Use a tabindex of 1 so that users can hit tab on any page and it will
# focus on the search widget.
q = forms.CharField(
required=False, label=_('Search'),
widget=SearchInput({
"placeholder": _('Search'),
"tabindex": "1",
"class": "form-control"
}))
# Search
RELEVANCY = "relevancy"
TOP_RATED = "rating"
NEWEST = "newest"
PRICE_HIGH_TO_LOW = "price-desc"
PRICE_LOW_TO_HIGH = "price-asc"
TITLE_A_TO_Z = "title-asc"
TITLE_Z_TO_A = "title-desc"
SORT_BY_CHOICES = [
(PRICE_LOW_TO_HIGH, _("Price low to high")),
(PRICE_HIGH_TO_LOW, _("Price high to low")),
(NEWEST, _("Newest")),
(TOP_RATED, _("Customer rating")),
]
# Map query params to sorting fields. Note relevancy isn't included here
# as we assume results are returned in relevancy order in the absence of an
# explicit sort field being passed to the search backend.
SORT_BY_MAP = {
TOP_RATED: '-rating',
NEWEST: '-date_created',
PRICE_HIGH_TO_LOW: '-price',
PRICE_LOW_TO_HIGH: 'price',
TITLE_A_TO_Z: 'title_s',
TITLE_Z_TO_A: '-title_s',
}
# Non Solr backends don't support dynamic fields so we just sort on title
if not is_solr_supported():
SORT_BY_MAP[TITLE_A_TO_Z] = 'title'
SORT_BY_MAP[TITLE_Z_TO_A] = '-title'
sort_by = forms.ChoiceField(
label=_("Sort by"), choices=SORT_BY_CHOICES,
widget=forms.Select(), required=False)
# Implementation of Price range filter based on:
# https://github.com/django-oscar/django-oscar/blob/master/src/oscar/apps/search/forms.py#L86
@property
def selected_multi_facets(self):
"""
Validate and return the selected facets
"""
# Process selected facets into a dict(field->[*values]) to handle
# multi-faceting
selected_multi_facets = defaultdict(list)
for facet_kv in self.selected_facets:
if ":" not in facet_kv:
continue
field_name, value = facet_kv.split(':', 1)
# EDITED PART comparing to original Oscar source
# Validate query facets as they as passed unescaped to Solr
if field_name in VALID_FACET_QUERIES:
if field_name in settings.OSCAR_SEARCH_FACETS['dynamic_queries_field_names']:
pass
else:
if value not in VALID_FACET_QUERIES[field_name]:
# Invalid query value
continue
# END
selected_multi_facets[field_name].append(value)
return selected_multi_facets
static/js/price_range_filter.js
看起来像:
$(document).ready(function() {
// Next vars are included in price_range_filter.html, as we need to provide data from that template to this js.
// var min_category_price = Number("{{ facet_data.price_range.results.min_category_price }}".replace(/\s/g,'')),
// max_category_price = Number("{{ facet_data.price_range.results.max_category_price }}".replace(/\s/g,'')),
// dynamic_query_fields = JSON.parse("{{ facet_data.price_range.results.dynamic_query_fields|escapejs }}"),
// current_url = "{{ request.get_full_path }}";
var category_url = current_url.split('/?selected_facets')[0],
min_filtered_price = 0,
max_filtered_price = 0;
// 1. Extracts queries (as key:value) from URL
// 2. Applies price range to Input Fields and Slider
// 3. Rebuilds 'submit' URL of price range
function handleUrl(use_globals_filtered_prices) {
//
var qd = {},
base_url_part = 'selected_facets=',
rebuilt_url ='?';
if (location.search) location.search.substr(1).split("&").forEach(function(item) {
var s = item.split("="),
k = s[0],
v = s[1] && decodeURIComponent(s[1]); // null-coalescing / short-circuit
//(k in qd) ? qd[k].push(v) : qd[k] = [v]
(qd[k] = qd[k] || []).push(v) // null-coalescing / short-circuit
});
// End of Whosebug
var facets = qd['selected_facets'],
price_changed = false;
for (var i in facets) {
var kv = facets[i],
k = kv.split(':')[0], // price_exact
v = kv.split(':')[1]; // [8732+TO+54432]
// Get filtered price range from URL and set Input Fields and Slider according to this range
// If k in dynamic_query_fields
if (dynamic_query_fields.indexOf(k) >= 0) {
// Replace existing price range in URL. Used when price range is changed
if (use_globals_filtered_prices){
kv = k + ':' + '[' + min_filtered_price + '+TO+' + max_filtered_price + ']';
price_changed = true;
}
// Just get min\max_filtered_prices and apply to Input Fields and Slider. Used when page is load
else {
min_filtered_price = v.substring(v.lastIndexOf("[")+1, v.lastIndexOf("+TO"));
max_filtered_price = v.substring(v.lastIndexOf("+TO+")+4, v.lastIndexOf("]"));
$('input.sliderValue[data-index="0"]').val(min_filtered_price);
$('input.sliderValue[data-index="1"]').val(max_filtered_price);
// 0 and 1 are field indexes
$("#slider").slider("values", 0, min_filtered_price);
$("#slider").slider("values", 1, max_filtered_price);
}
}
rebuilt_url += base_url_part + kv + '&';
}
// When we set price range at the first time, i.e when there is no previous version of price range facet.
if (use_globals_filtered_prices && !price_changed) {
kv = base_url_part + 'price_exact' + ':' + '[' + min_filtered_price + '+TO+' + max_filtered_price + ']';
rebuilt_url += kv;
}
if (rebuilt_url.slice(-1) === '&') {
rebuilt_url = rebuilt_url.slice(0, -1);
}
// If facets not selected
if (rebuilt_url !== '?') {
var full_url = category_url + encodeURI(rebuilt_url).replace(/:\s*/g, "%3A");
$("#submit_price").attr("href", full_url);
}
}
// SLIDER
$("#slider").slider({
min: min_category_price,
max: max_category_price,
step: 100,
range: true,
values: [min_category_price, max_category_price],
// After sliders are moved, change Input Field Values
slide: function(event, ui) {
for (var i = 0; i < ui.values.length; ++i) {
$("input.sliderValue[data-index=" + i + "]").val(ui.values[i]);
if (i === 0){
min_filtered_price = ui.values[i];
}
else {
max_filtered_price = ui.values[i]
}
handleUrl(true);
}
}
});
// INPUT FIELDS
$("input.sliderValue").change(function() {
var $this = $(this),
changed_field = $this.data("index"),
changed_price = $this.val();
$("#slider").slider("values", changed_field, changed_price);
if (changed_field === 0){
min_filtered_price = changed_price;
//Fix "0" max range URL price when just min range is changed
if (max_filtered_price === 0){
max_filtered_price = max_category_price;
}
}
else {
//Fix "0" min range URL price when just max range is changed
if (min_filtered_price === 0){
min_filtered_price = min_category_price;
}
max_filtered_price = changed_price;
}
handleUrl(true);
});
// # Executes once the page is loaded
handleUrl(false);
});
facets template
扩展了 category template
(客户看到产品的地方)并包含 price range filter
的 html 代码:
{% extends "catalogue/category.html" %}
{% block category_facets %}
{% if facet_data.price_range.results %}
{% include 'search/partials/price_range_filter.html' %}
{% endif %}
{% with facet_data.vendor as data %}
{% if data.results %}
{% include 'search/partials/facet.html' with name=data.name items=data.results %}
{% endif %}
{% endwith %}
{# OTHET FACETS #}
{% endblock %}
创建root/templates/search/partials/price_range_filter.html
。这看起来像 Oscar 的结构,但不会覆盖任何内容,因为 Oscar 没有 price_range_filter.html
。我决定在这里删除 price_range_filter.html
因为 Oscar 通常负责过滤器。
price_range_filter.html
看起来像(将样式放入 css,如果你愿意的话:)):
{% load staticfiles %}
<dl>
<dt class="nav-header">{{ facet_data.price_range.name }}</dt>
<div style="display: flex;">
<input type="text" class="sliderValue" data-index="0"
value="{{ facet_data.price_range.results.min_category_price }}"
style="width: 70px; margin-right: 10px"/>
<input type="text" class="sliderValue" data-index="1"
value="{{ facet_data.price_range.results.max_category_price }}"
style="width: 70px; margin-right: 10px"/>
<a id="submit_price" href="" class="btn btn-default">OK</a>
</div>
<br />
<div id="slider"></div>
</dl>
{% block extrascripts %}
<script>
var min_category_price = Number("{{ facet_data.price_range.results.min_category_price }}".replace(/\s/g,'')),
max_category_price = Number("{{ facet_data.price_range.results.max_category_price }}".replace(/\s/g,'')),
dynamic_query_fields = JSON.parse("{{ facet_data.price_range.results.dynamic_query_fields|escapejs }}"),
current_url = "{{ request.get_full_path }}";
</script>
<script type="text/JavaScript" src="{% static 'js/price_range_filter.js' %}"></script>
{% endblock %}
我不是 'pro' 程序员,所以欢迎 advices\improvements :)
奖金: