【问题标题】:Need advice about a proper way how to make a price range filter (price slider)需要有关如何制作价格范围过滤器的正确方法的建议(价格滑块)
【发布时间】:2017-10-13 15:17:32
【问题描述】:

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)]

我知道如何获取SOME_MIN_PRICE 和SOME_MAX_PRICE,但在这里我遇到了url 的问题,它过滤了产品-> 我找不到方法,如何为此生成有效的url动态方面。

例如,如果我在浏览器中手动更改范围(例如在查询 ?selected_facets=price_exact%3A%5B0+TO+1000%5D 中,我将 1000 更改为 1001),Oscar 会返回我所在类别的所有产品。

谁能告诉我带有url的解决方案,如果总体上有更好的方法,请指出方向?

【问题讨论】:

    标签: django solr django-haystack django-oscar


    【解决方案1】:

    首先我想说这个方法很脏,特别是在需要在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 %}
    

    我不是“专业”编码员,所以欢迎任何建议\改进:)

    奖励:

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-10-23
      • 1970-01-01
      • 2016-04-11
      • 1970-01-01
      • 2021-12-20
      • 2020-12-26
      • 1970-01-01
      • 2016-09-26
      相关资源
      最近更新 更多