Skip to content

Commit

Permalink
add search_scope
Browse files Browse the repository at this point in the history
  • Loading branch information
RubyDiver committed Sep 1, 2021
1 parent aa552ca commit ce1b0c7
Show file tree
Hide file tree
Showing 7 changed files with 402 additions and 50 deletions.
344 changes: 344 additions & 0 deletions app/models/concerns/spree/product_scopes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,344 @@
module Spree
module ProductScopes

This comment has been minimized.

Copy link
@chytreg

chytreg Sep 2, 2021

Member

Imho would be better to open Spree::Product class instead of ProductScopes based on https://dev-docs.spreecommerce.org/customization/logic or simple class_eval

extend ActiveSupport::Concern

included do
cattr_accessor :search_scopes do
[]
end

def self.add_search_scope(name, &block)
singleton_class.send(:define_method, name.to_sym, &block)
search_scopes << name.to_sym
end

def self.simple_scopes
[
:ascend_by_updated_at,
:descend_by_updated_at,
:ascend_by_name,
:descend_by_name,
:ascend_by_postcode
]
end

def self.add_simple_scopes(scopes)
scopes.each do |name|
# We should not define price scopes here, as they require something slightly different
next if name.to_s.include?('master_price')

parts = name.to_s.match(/(.*)_by_(.*)/)
scope(name.to_s, -> { order(Arel.sql("#{Product.quoted_table_name}.#{parts[2]} #{parts[1] == 'ascend' ? 'ASC' : 'DESC'}")) })
end
end

def self.property_conditions(property)
properties = Property.table_name
case property
when String then { "#{properties}.name" => property }
when Property then { "#{properties}.id" => property.id }
else { "#{properties}.id" => property.to_i }
end
end

add_simple_scopes simple_scopes

add_search_scope :ascend_by_master_price do
order("#{price_table_name}.amount ASC")
end

add_search_scope :descend_by_master_price do
order("#{price_table_name}.amount DESC")
end

add_search_scope :price_between do |low, high|
where(Price.table_name => { amount: low..high })
end

add_search_scope :master_price_lte do |price|
where("#{price_table_name}.amount <= ?", price)
end

add_search_scope :master_price_gte do |price|
where("#{price_table_name}.amount >= ?", price)
end

add_search_scope :in_stock do
joins(:variants_including_master).merge(Spree::Variant.in_stock)
end

add_search_scope :backorderable do
joins(:variants_including_master).merge(Spree::Variant.backorderable)
end

add_search_scope :in_stock_or_backorderable do
joins(:variants_including_master).merge(Spree::Variant.in_stock_or_backorderable)
end


# Select nearest vendors
shop = Spree::Vendor.first

# Create CASE based on vendor_id table
#h = shop
#x = {}
#h.each_with_index {|v, i| x[i] = v}
#h
#x.sort_by {|_key, value| value}.to_h
order_sql
vendor_ids = shop.nearbys;
vendors_ids.map {|id| "WHEN spree_product.vendor_id = '#{id} then ${index}'"

# Add search scope
add_search_scope :ascend_by_postcode do

This comment has been minimized.

Copy link
@chytreg

chytreg Sep 2, 2021

Member

It should take an argument, and the logic from the above should be placed inside this method.
When add_search_scope is a problem for you on this module-level you can reopen Spree::Product class and add scope like:

Spree::Product.class_eval do
   def self.ascend_by_postcode(postcode)
      # geocode postcode / or address if this ie easier for you
      # sort vendors in the order closest to the given postcode 
      # based on vendors array generate order SQL
      # result be smth like "CASE WHEN spree_product.vendor_id = '5' then 0 WHEN spree_product.vendor_id = '4' THEN 1 ELSE 2" 
      order_sql = "generated case when statemtn here"
      joins(master: :default_price).order(Arel.sql(order_sql)).all
   end
end

Spree::Product.ascend_by_postcode(postcode) -> should return products

This comment has been minimized.

Copy link
@chytreg

chytreg Sep 2, 2021

Member

Looks like class_eval won't work because of Zeitwerk error this is a working solution

# models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      def base.ascend_by_postcode(postcode)
        puts "postcode"
        #     # geocode postcode / or address if this ie easier for you
        #     # sort vendors in the order closest to the given postcode
        #     # based on vendors array generate order SQL
        #     # result be smth like "CASE WHEN spree_product.vendor_id = '5' then 0 WHEN spree_product.vendor_id = '4' THEN 1 ELSE 2"
        #     order_sql = "generated case when statemtn here"
        #     joins(master: :default_price).order(Arel.sql(order_sql))
        joins(master: :default_price)
      end
    end
  end
end

::Spree::Product.prepend Spree::ProductDecorator if ::Spree::Product.included_modules.exclude?(Spree::ProductDecorator)
Spree::Product.joins(master: :default_price).order(Arel.sql()).all
end


# vendor_ids= [4,6,3,1]; order_sql = "CASE"; vendors_ids.map {|id| "WHEN spree_product.vendor_id = '#{id} then ${index}'"
# "Case" + n*"WHEN then 1" + "ELSE"
# "CASE #{tbl.join(" ")} ELSE #{vendor_ids.size} END"


# This scope selects products in taxon AND all its descendants
# If you need products only within one taxon use
#
# Spree::Product.joins(:taxons).where(Taxon.table_name => { id: taxon.id })
#
# If you're using count on the result of this scope, you must use the
# `:distinct` option as well:
#
# Spree::Product.in_taxon(taxon).count(distinct: true)
#
# This is so that the count query is distinct'd:
#
# SELECT COUNT(DISTINCT "spree_products"."id") ...
#
# vs.
#
# SELECT COUNT(*) ...
add_search_scope :in_taxon do |taxon|
includes(:classifications).
where('spree_products_taxons.taxon_id' => taxon.cached_self_and_descendants_ids).
order('spree_products_taxons.position ASC')
end

# This scope selects products in all taxons AND all its descendants
# If you need products only within one taxon use
#
# Spree::Product.taxons_id_eq([x,y])
add_search_scope :in_taxons do |*taxons|
taxons = get_taxons(taxons)
taxons.first ? prepare_taxon_conditions(taxons) : where(nil)
end

add_search_scope :ascend_by_taxons_min_position do |taxon_ids|
joins(:classifications).
where(Classification.table_name => { taxon_id: taxon_ids }).
select(
[
"#{Product.table_name}.*",
"MIN(#{Classification.table_name}.position) AS min_position"
].join(', ')
).
group(:id).
order(min_position: :asc)
end

# a scope that finds all products having property specified by name, object or id
add_search_scope :with_property do |property|
joins(:properties).where(property_conditions(property))
end

# a simple test for product with a certain property-value pairing
# note that it can test for properties with NULL values, but not for absent values
add_search_scope :with_property_value do |property, value|
joins(:properties).
where("#{ProductProperty.table_name}.value = ?", value).
where(property_conditions(property))
end

add_search_scope :with_property_values do |property_filter_param, property_values|
joins(product_properties: :property).
where(Property.table_name => { filter_param: property_filter_param }).
where(ProductProperty.table_name => { filter_param: property_values.map(&:parameterize) })
end

add_search_scope :with_option do |option|
option_types = OptionType.table_name
conditions = case option
when String then { "#{option_types}.name" => option }
when OptionType then { "#{option_types}.id" => option.id }
else { "#{option_types}.id" => option.to_i }
end

joins(:option_types).where(conditions)
end

add_search_scope :with_option_value do |option, value|
option_values = OptionValue.table_name
option_type_id = case option
when String then OptionType.find_by(name: option) || option.to_i
when OptionType then option.id
else option.to_i
end

conditions = "#{option_values}.name = ? AND #{option_values}.option_type_id = ?", value, option_type_id
group('spree_products.id').joins(variants_including_master: :option_values).where(conditions)
end

# Finds all products which have either:
# 1) have an option value with the name matching the one given
# 2) have a product property with a value matching the one given
add_search_scope :with do |value|
includes(variants_including_master: :option_values).
includes(:product_properties).
where("#{OptionValue.table_name}.name = ? OR #{ProductProperty.table_name}.value = ?", value, value)
end

# Finds all products that have a name containing the given words.
add_search_scope :in_name do |words|
like_any([:name], prepare_words(words))
end

# Finds all products that have a name or meta_keywords containing the given words.
add_search_scope :in_name_or_keywords do |words|
like_any([:name, :meta_keywords], prepare_words(words))
end

# Finds all products that have a name, description, meta_description or meta_keywords containing the given keywords.
add_search_scope :in_name_or_description do |words|
like_any([:name, :description, :meta_description, :meta_keywords], prepare_words(words))
end

# Finds all products that have the ids matching the given collection of ids.
# Alternatively, you could use find(collection_of_ids), but that would raise an exception if one product couldn't be found
add_search_scope :with_ids do |*ids|
where(id: ids)
end

# Sorts products from most popular (popularity is extracted from how many
# times use has put product in cart, not completed orders)
#
# there is alternative faster and more elegant solution, it has small drawback though,
# it doesn stack with other scopes :/
#
# joins: "LEFT OUTER JOIN (SELECT line_items.variant_id as vid, COUNT(*) as cnt FROM line_items GROUP BY line_items.variant_id) AS popularity_count ON variants.id = vid",
# order: 'COALESCE(cnt, 0) DESC'
add_search_scope :descend_by_popularity do
joins(:master).
order(%Q{
COALESCE((
SELECT
COUNT(#{LineItem.quoted_table_name}.id)
FROM
#{LineItem.quoted_table_name}
JOIN
#{Variant.quoted_table_name} AS popular_variants
ON
popular_variants.id = #{LineItem.quoted_table_name}.variant_id
WHERE
popular_variants.product_id = #{Product.quoted_table_name}.id
), 0) DESC
})
end

add_search_scope :not_deleted do
where("#{Product.quoted_table_name}.deleted_at IS NULL or #{Product.quoted_table_name}.deleted_at >= ?", Time.zone.now)
end

def self.not_discontinued(only_not_discontinued = true)
if only_not_discontinued != '0' && only_not_discontinued
where("#{Product.quoted_table_name}.discontinue_on IS NULL or #{Product.quoted_table_name}.discontinue_on >= ?", Time.zone.now)
else
all
end
end
search_scopes << :not_discontinued

def self.with_currency(currency)
joins(variants_including_master: :prices).
where(Price.table_name => { currency: currency.upcase }).
where.not(Price.table_name => { amount: nil }).
distinct
end
search_scopes << :with_currency

# Can't use add_search_scope for this as it needs a default argument
def self.available(available_on = nil, currency = nil)
available_on ||= Time.current

scope = not_discontinued.where("#{Product.quoted_table_name}.available_on <= ?", available_on)

unless Spree::Config.show_products_without_price
currency ||= Spree::Config[:currency]
scope = scope.with_currency(currency)
end

scope
end
search_scopes << :available

def self.active(currency = nil)
available(nil, currency)
end
search_scopes << :active

def self.for_filters(currency, taxon: nil)
scope = active(currency)
scope = scope.in_taxon(taxon) if taxon.present?
scope
end
search_scopes << :for_filters

def self.for_user(user = nil)
if user.try(:has_spree_role?, 'admin')
with_deleted
else
not_deleted.not_discontinued.where("#{Product.quoted_table_name}.available_on <= ?", Time.current)
end
end

add_search_scope :taxons_name_eq do |name|
group('spree_products.id').joins(:taxons).where(Taxon.arel_table[:name].eq(name))
end

def self.price_table_name
Price.quoted_table_name
end
private_class_method :price_table_name

# specifically avoid having an order for taxon search (conflicts with main order)
def self.prepare_taxon_conditions(taxons)
ids = taxons.map(&:cached_self_and_descendants_ids).flatten.uniq
joins(:classifications).where(Classification.table_name => { taxon_id: ids })
end
private_class_method :prepare_taxon_conditions

# Produce an array of keywords for use in scopes.
# Always return array with at least an empty string to avoid SQL errors
def self.prepare_words(words)
return [''] if words.blank?

a = words.split(/[,\s]/).map(&:strip)
a.any? ? a : ['']
end
private_class_method :prepare_words

def self.get_taxons(*ids_or_records_or_names)
taxons = Taxon.table_name
ids_or_records_or_names.flatten.map do |t|
case t
when Integer then Taxon.find_by(id: t)
when ApplicationRecord then t
when String
Taxon.find_by(name: t) ||
Taxon.where("#{taxons}.permalink LIKE ? OR #{taxons}.permalink = ?", "%/#{t}/", "#{t}/").first
end
end.compact.flatten.uniq
end
private_class_method :get_taxons
end
end
end

21 changes: 0 additions & 21 deletions app/models/spree/product_sorting_decorator.rb

This file was deleted.

18 changes: 0 additions & 18 deletions app/models/spree/user_decorator.rb

This file was deleted.

4 changes: 2 additions & 2 deletions app/views/spree/products/_sort_desktop.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
<li class="p-3 plp-sort-dropdown-ul-li<%= '--active' if params[:sort_by] == 'price-low-to-high' %>">
<%= link_to Spree.t('plp.price_low_to_high'), url_for(permitted_params.merge(sort_by: 'price-low-to-high')), class: 'dropdown-item' %>
</li>
<li class="p-3 plp-sort-dropdown-ul-li<%= '--active' if params[:sort_by] == 'postcode' %>">
<%= link_to Spree.t('plp.postcode'), url_for(permitted_params.merge(sort_by: 'postcode')), class: 'dropdown-item' %>
<li class="p-3 plp-sort-dropdown-ul-li<%= '--active' if params[:sort_by] == 'ascend_by_postcode' %>">
<%= link_to Spree.t('plp.postcode'), url_for(permitted_params.merge(sort_by: 'ascend_by_postcode')), class: 'dropdown-item' %>
</li>
</ul>
</div>
Expand Down
Loading

0 comments on commit ce1b0c7

Please sign in to comment.