core/app/models/concerns/spree/product_scopes.rb
module Spree
module ProductScopes
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
]
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(sanitize_sql("#{Product.quoted_table_name}.#{parts[2]} #{parts[1] == 'ascend' ? 'ASC' : 'DESC'}"))) })
end
end
def self.property_conditions(property)
properties_table = Property.table_name
property_translations_table = Property.translation_table_alias
case property
when Property then { "#{properties_table}.id" => property.id }
when Integer then { "#{properties_table}.id" => property }
else
if Property.column_for_attribute('id').type == :uuid
["#{property_translations_table.name} = ? OR #{properties_table.id} = ?", property, property]
else
{ "#{property_translations_table}.name" => property }
end
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
# 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).join_translation_table(Property).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).
join_translation_table(Property).
join_translation_table(ProductProperty).
where("#{ProductProperty.translation_table_alias}.value = ?", value).
where(property_conditions(property))
end
add_search_scope :with_property_values do |property_filter_param, property_values|
joins(product_properties: :property).
join_translation_table(Property).
join_translation_table(ProductProperty).
where(Property.translation_table_alias => { filter_param: property_filter_param }).
where(ProductProperty.translation_table_alias => { filter_param: property_values.map(&:parameterize) })
end
add_search_scope :with_option do |option|
if option.is_a?(OptionType)
joins(:option_types).where(spree_option_types: { id: option.id })
elsif option.is_a?(Integer)
joins(:option_types).where(spree_option_types: { id: option })
elsif OptionType.column_for_attribute('id').type == :uuid
joins(:option_types).where(spree_option_types: { name: option }).or(Product.joins(:option_types).where(spree_option_types: { id: option }))
else
joins(:option_types).
join_translation_table(OptionType).
where(OptionType.translation_table_alias => { name: option })
end
end
add_search_scope :with_option_value do |option, value|
option_type_id = case option
when OptionType then option.id
when Integer then option
else
if OptionType.column_for_attribute('id').type == :uuid
OptionType.where(id: option).or(OptionType.where(name: option))&.first&.id
else
OptionType.where(name: option)&.first&.id
OptionType.where(name: option)&.first&.id
end
end
return Product.group("#{Spree::Product.table_name}.id").none if option_type_id.blank?
group("#{Spree::Product.table_name}.id").
joins(variants_including_master: :option_values).
join_translation_table(Spree::OptionValue).
where(Spree::OptionValue.translation_table_alias => { name: value },
Spree::OptionValue.table_name => { option_type_id: option_type_id })
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.not(status: 'archived')
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)
if available_on
scope = not_discontinued.where("#{Product.quoted_table_name}.available_on <= ?", available_on)
else
scope = where(status: 'active')
end
unless Spree::Config.show_products_without_price
currency ||= Spree::Store.default.default_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.where(status: 'active')
end
end
add_search_scope :taxons_name_eq do |name|
group('spree_products.id').joins(:taxons).where(Taxon.arel_table[:name].eq(name))
end
# .search_by_name
if defined?(PgSearch)
include PgSearch::Model
pg_search_scope :search_by_name, against: :name, using: { tsearch: { any_word: true, prefix: true } }
else
def self.search_by_name(query)
i18n { name.lower.matches("%#{query.downcase}%") }
end
end
search_scopes << :search_by_name
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)
ids_or_records_or_names.flatten.map do |t|
case t
when ApplicationRecord then t
else
Taxon.where(name: t).
or(Taxon.where(Taxon.arel_table[:id].eq(t))).
or(Taxon.where(Taxon.arel_table[:permalink].matches("%/#{t}/"))).
or(Taxon.where(Taxon.arel_table[:permalink].matches("#{t}/"))).first
end
end.compact.flatten.uniq
end
private_class_method :get_taxons
end
end
end