首先我想说这个方法很脏,特别是在需要在js中准备URL才能应用价格范围的部分。如果有人知道或希望通过 Oscar\Haystack 代码实现可行的 URL - 欢迎。
小提示:我不知道它是由 Oscar 设计的还是我当前项目的上一个开发人员决定的,但我的模型具有下一个结构
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 模型有一个前端产品类别,只有 CPU。不能在一个类别中混用不同类型的产品。
基于这个模型结构,很难找出客户属于哪个类别,因为下面的search_handlers.py 中的self.categories[0].product_set.first() 返回了Product 的实例,这是不合适的,因为我需要CPU、Cooler 等实例为了定义客户所在类别的最低\最高价格。
开始吧
阅读cmets内部代码了解详情。
某处(可能是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 文件。在哪里创作?如果您不知道,则可能在您的“项目”文件夹中,即。在您的“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) {
// https://stackoverflow.com/a/21152762/4992248
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 StackOverflow
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 %}
我不是“专业”编码员,所以欢迎任何建议\改进:)
奖励: