按 Spree 中的选项过滤产品

Filtering Products by an Option in Spree

使用 Spree Commerce 3.0-stable,我需要编写自定义产品过滤器以仅显示至少一个变体与所选 OptionValue 匹配的产品。

我有一个过滤器可以在复选框中显示正确的选项列表,但选择一个选项不会改变退回的产品。

对于此示例,产品有多个 "Metal" 选项(铂金、白金、黄金、银等)。我已经设置了价格范围过滤器,并且它工作正常。

如何让产品按选项过滤?

我的lib/spree/product_filters.rb

module Spree
  module Core
    module ProductFilters
      # Example: filtering by price
      #   The named scope just maps incoming labels onto their conditions, and builds the conjunction
      #   'price' is in the base scope's context (ie, "select foo from products where ...") so
      #     we can access the field right away
      #   The filter identifies which scope to use, then sets the conditions for each price range
      #
      # If user checks off three different price ranges then the argument passed to
      # below scope would be something like [" - ", " - ", " - "]
      #
      Spree::Product.add_search_scope :price_range_any do |*opts|
        conds = opts.map {|o| Spree::Core::ProductFilters.price_filter[:conds][o]}.reject { |c| c.nil? }
        scope = conds.shift
        conds.each do |new_scope|
          scope = scope.or(new_scope)
        end
        Spree::Product.joins(master: :default_price).where(scope)
      end

      def ProductFilters.format_price(amount)
        Spree::Money.new(amount)
      end

      def ProductFilters.price_filter
        v = Spree::Price.arel_table
        conds = [ [ Spree.t(:under_price, price: format_price(1000))     , v[:amount].lteq(1000)],
                  [ "#{format_price(1000)} - #{format_price(1500)}"        , v[:amount].in(1000..1500)],
                  [ "#{format_price(1500)} - #{format_price(1800)}"        , v[:amount].in(1500..1800)],
                  [ "#{format_price(1800)} - #{format_price(2000)}"        , v[:amount].in(1800..2000)],
                  [ Spree.t(:or_over_price, price: format_price(2000)) , v[:amount].gteq(2000)]]
        {
          name:   Spree.t(:price_range),
          scope:  :price_range_any,
          conds:  Hash[*conds.flatten],
          labels: conds.map { |k,v| [k, k] }
        }
      end



      # Test for discrete option values selection
      def ProductFilters.option_with_values(option_scope, option, values)
        # get values IDs for Option with name {@option} and value-names in {@values} for use in SQL below
        option_values = Spree::OptionValue.where(:presentation => [values].flatten).joins(:option_type).where(OptionType.table_name => {:name => option}).pluck("#{OptionValue.table_name}.id")
        return option_scope if option_values.empty?

        option_scope = option_scope.where("#{Product.table_name}.id in (select product_id from #{Variant.table_name} v left join spree_option_values_variants ov on ov.variant_id = v.id where ov.option_value_id in (?))", option_values)
        option_scope
        puts option_scope.inspect
      end

      # multi-option scope
      Spree::Product.scope :option_any,
                         lambda { |*opts|
                           option_scope = Spree::Product.includes(:variants_including_master)
                           opts.map { |opt|
                             # opt is an array => ['option-name', [value1, value2, value3, ...]]
                             option_scope = option_with_values(option_scope, *opt)
                           }
                           option_scope
                         }

      # metal filter
      def ProductFilters.metal_filter
        metals = Spree::OptionValue.where( :option_type_id => Spree::OptionType.find_by!(name: "Metal") ).order("position").map(&:presentation).compact.uniq
        {
            :name => "Metal Type",
            :scope => :option_any,
            :conds => nil,
            :option => 'metal',
            :labels => metals.map { |k| [k, k] }
        }
      end

    end
  end
end

我的app/views/spree/home/index.html.erb

<% content_for :sidebar do %>
  <div data-hook="homepage_sidebar_navigation">
    <%= render :partial => 'spree/shared/filters' %>
    <%= render :partial => 'spree/shared/taxonomies' %>
  </div>
<% end %>
<h2>Test!</h2>
<div data-hook="homepage_products">
  <% cache(cache_key_for_products) do %>
    <%= render :partial => 'spree/shared/products', :locals => { :products => @products } %>
  <% end %>
</div>

我的app/views/spree/shared/_filters.html.erb

<% filters = [Spree::Core::ProductFilters.metal_filter,Spree::Core::ProductFilters.price_filter] %>

<% unless filters.empty? %>
  <%= form_tag '', :method => :get, :id => 'sidebar_products_search' do %>
    <%= hidden_field_tag 'per_page', params[:per_page] %>
    <% filters.each do |filter| %> <i><%= filter[:name] %> </i>
      <% labels = filter[:labels] || filter[:conds].map {|m,c| [m,m]} %>
      <% next if labels.empty? %>
      <div class="navigation" data-hook="navigation">
        <h4 class="filter-title"> <%= filter[:name] %> </h4>
        <ul class="list-group">
          <% labels.each do |nm,val| %>
            <% label = "#{filter[:name]}_#{nm}".gsub(/\s+/,'_') %>
            <li class="list-group-item">
              <input type="checkbox"
                     id="<%= label %>"
                     name="search[<%= filter[:scope].to_s %>][]"
                     value="<%= val %>"
                     <%= params[:search] && params[:search][filter[:scope]] && params[:search][filter[:scope]].include?(val.to_s) ? "checked" : "" %> />
              <label class="nowrap" for="<%= label %>"> <%= nm %> </label>
            </li>
          <% end %>
        </ul>
      </div>
    <% end %>
    <%= submit_tag Spree.t(:search), :name => nil, :class => 'btn btn-primary' %>
  <% end %>
<% end %>

我不知道 Spree::Product.scope 到底在做什么,但请尝试将其更改为 Spree::Product.add_search_scope。您还缺少 option_with_values 中的选项类型参数,您可以使用 ProductFilters.metal_filter[:option]。

Spree::Product.add_search_scope :option_any do |*opts|
  option_scope = Spree::Product.includes(:variants_including_master)
  option_type = ProductFilters.metal_filter[:option]

  opts.map { |opt|
    # opt is an array => ['option-name', [value1, value2, value3, ...]]
    option_scope = ProductFilters.option_with_values(option_scope, option_type, *opt)
   }
   option_scope
end

David Gross 上面的回答对我有用,尽管我使用的是颜色选项。这是我的代码的样子以及我为让它工作所采取的步骤。

1) 将 product_filters.rb 的未编辑版本复制到 lib/product_filters.rb

2) 初始化:在initializers/spree.rb中加入:

require 'product_filters'

# Spree.config do |config| etc........

3) 将此代码添加到 product_filters.rb:

  def ProductFilters.option_with_values(option_scope, option, values)
    # get values IDs for Option with name {@option} and value-names in {@values} for use in SQL below
    option_values = Spree::OptionValue.where(:presentation => [values].flatten).joins(:option_type).where(OptionType.table_name => {:name => option}).pluck("#{OptionValue.table_name}.id")
    return option_scope if option_values.empty?

    option_scope = option_scope.where("#{Product.table_name}.id in (select product_id from #{Variant.table_name} v left join spree_option_values_variants ov on ov.variant_id = v.id where ov.option_value_id in (?))", option_values)
    option_scope
  end

  # option scope
  Spree::Product.add_search_scope :option_any do |*opts|
    option_scope = Spree::Product.includes(:variants_including_master)
    option_type = ProductFilters.colour_filter[:option]

    opts.map { |opt|
      # opt is an array => ['option-name', [value1, value2, value3, ...]]
      option_scope = ProductFilters.option_with_values(option_scope, option_type, *opt)
     }
     option_scope
  end

  # colour option - object that describes the filter.
  def ProductFilters.colour_filter
    # Get an array of possible colours (option type of 'colour')
    # e.g. returns ["Gold", "Black", "White", "Silver", "Purple", "Multicoloured"]
    colours = Spree::OptionValue.where(:option_type_id => Spree::OptionType.find_by_name("colour")).order("position").map(&:presentation).compact.uniq
    {
        :name => "Colour",
        :scope => :option_any,
        :conds => nil,
        :option => 'colour', # this is MANDATORY
        :class => "colour",
        :labels => colours.map { |k| [k, k] }
    }
  end

4) 将您的新过滤器添加到 app/models/spree/taxons.rb,使其出现在前端:

def applicable_filters
  fs = []
  # fs << ProductFilters.taxons_below(self)
  ## unless it's a root taxon? left open for demo purposes

  fs << Spree::Core::ProductFilters.price_filter if Spree::Core::ProductFilters.respond_to?(:price_filter)
  fs << Spree::Core::ProductFilters.brand_filter if Spree::Core::ProductFilters.respond_to?(:brand_filter)
  fs << Spree::Core::ProductFilters.colour_filter if Spree::Core::ProductFilters.respond_to?(:colour_filter)
  fs
end

应该是这样。我希望这有帮助 - 如果我能提供进一步帮助,请告诉我。不幸的是,Spree 过滤文档不存在,所以我们不得不凑合。