Reputation: 1599
With Spree Commerce 3.0-stable, I need to write a custom product filter to show only products where at least one variant matches the selected OptionValue.
I've got a filter that displays the correct list of options in the checkboxes, but selecting an option doesn't change which products are returned.
For this example, Products are available in multiple "Metal" options (platinum, white gold, yellow gold, silver, etc.). I've got the price range filter set up, and it's working correctly.
How can I get the products to filter by an Option?
My 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 ["$10 - $15", "$15 - $18", "$18 - $20"]
#
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
My 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>
My 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 %>
Upvotes: 1
Views: 3131
Reputation: 484
David Gross's answer above worked for me, although I am using the option of colour. Here's what my code looks like and the steps I took to get it to work.
1) Copy an unedited version of product_filters.rb to lib/product_filters.rb
2) Initialise it: in initializers/spree.rb, add in:
require 'product_filters'
# Spree.config do |config| etc........
3) Add this code to 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) Add your new filter to app/models/spree/taxons.rb so it appears on the front end:
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
That should be it. I hope that helps - let me know if I can help further. Unfortunately the Spree filtering docs are nonexistent so we have to make do.
Upvotes: 3
Reputation: 1873
I dont know exactly what Spree::Product.scope is doing but try changing it to Spree::Product.add_search_scope. You are also missing an argument OptionType in option_with_values, you can use 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
Upvotes: 1