按 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 过滤文档不存在,所以我们不得不凑合。
使用 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 过滤文档不存在,所以我们不得不凑合。