2017-10-13 119 views
0

Oscar有小配置的這樣的結構:約有道需要諮詢如何做一個價格範圍過濾器(價格滑塊)

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是「靜態」,我想使它成爲一個動態的依賴在一個類別裏面的產品的價格。

基於該OSCAR_SEARCH_FACETS,奧斯卡使用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)] 

其中I知道如何獲得SOME_MIN_PRICESOME_MAX_PRICE,但在這裏我遇到了一個url問題,它過濾了一個產品 - >我找不到方法,我如何爲這個動態構面生成工作網址。

例如,如果我在瀏覽器中手動更改範圍(例如在查詢?selected_facets=price_exact%3A%5B0+TO+1000%5D中,我將1000更改爲1001),Oscar將返回所有類別的產品。

任何人都可以告訴我的解決方案與網址,如果總體有一個更好的方法,指示方向?

回答

0

首先我想說的是,這種方法很髒,特別是在需要在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.pyself.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 templatecategory 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 %} 

我不是一個 '親' 編碼器,因此任何意見\的改進是歡迎的。

紅利:

django oscar price range filter