首先我想說的是,這種方法很髒,特別是在需要在js中準備URL以應用價格範圍的部分。如果有人知道或希望通過Oscar \ Haystack代碼實施可行的網址 - 歡迎。
小注:如果它是由奧斯卡設計或我的當前項目的前開發決定這一點,但我的模型有一個結構
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等實例定義cliet所在類別的min \ max價格。
讓我們開始
閱讀評論的內部代碼的細節。
某處(可能base.html
)降:
<script type="text/JavaScript" src="{% static 'soberisam/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
。在哪裏創建?如果你不知道,甚至可能在你的'項目'文件夾內,即。在你的'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
。這看起來像奧斯卡的結構,但不會覆蓋任何東西,因爲奧斯卡沒有諸如price_range_filter.html
。我決定在這裏放棄price_range_filter.html
,因爲奧斯卡一般負責過濾器。
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 %}
我不是一個 '親' 編碼器,因此任何意見\的改進是歡迎的。
紅利: