Module: Spree::Product::Scopes

Defined in:
core/app/models/spree/product/scopes.rb

Class Method Summary collapse

Class Method Details

.prepended(base) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'core/app/models/spree/product/scopes.rb', line 6

def self.prepended(base)
  base.class_eval 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.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

    scope :ascend_by_updated_at, -> { order(updated_at: :asc) }
    scope :descend_by_updated_at, -> { order(updated_at: :desc) }
    scope :ascend_by_name, -> { order(name: :asc) }
    scope :descend_by_name, -> { order(name: :desc) }

    add_search_scope :ascend_by_master_price do
      joins(master: :default_price).order(Spree::Price.arel_table[:amount].asc)
    end

    add_search_scope :descend_by_master_price do
      joins(master: :default_price).order(Spree::Price.arel_table[:amount].desc)
    end

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

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

    add_search_scope :master_price_gte do |price|
      joins(master: :default_price).where("#{price_table_name}.amount >= ?", price)
    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.self_and_descendants.pluck(:id))
        .select(Spree::Classification.arel_table[:position])
        .order(Spree::Classification.arel_table[: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

    # 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("#{Spree::ProductProperty.table_name}.value = ?", value)
        .where(property_conditions(property))
    end

    add_search_scope :with_option do |option|
      option_types = Spree::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 = Spree::OptionValue.table_name
      option_type_id = case option
                       when String then Spree::OptionType.find_by(name: option) || option.to_i
                       when Spree::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("#{Spree::OptionValue.table_name}.name = ? OR #{Spree::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(%{
     COALESCE((
       SELECT
         COUNT(#{Spree::LineItem.quoted_table_name}.id)
       FROM
         #{Spree::LineItem.quoted_table_name}
       JOIN
         #{Spree::Variant.quoted_table_name} AS popular_variants
       ON
         popular_variants.id = #{Spree::LineItem.quoted_table_name}.variant_id
       WHERE
         popular_variants.product_id = #{Spree::Product.quoted_table_name}.id
     ), 0) DESC
  })
    end

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

    scope :with_master_price, -> do
      joins(:master).where(Spree::Price.where(Spree::Variant.arel_table[:id].eq(Spree::Price.arel_table[:variant_id])).arel.exists)
    end
    # Can't use add_search_scope for this as it needs a default argument
    def self.available(available_on = nil)
      with_master_price.where("#{Spree::Product.quoted_table_name}.available_on <= ?", available_on || Time.current)
    end
    search_scopes << :available

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

    def self.with_variant_sku_cont(sku)
      sku_match = "%#{sku}%"
      variant_table = Spree::Variant.arel_table
      subquery = Spree::Variant.where(variant_table[:sku].matches(sku_match).and(variant_table[:product_id].eq(arel_table[:id])))
      where(subquery.arel.exists)
    end

    def self.distinct_by_product_ids(sort_order = nil)
      Spree::Deprecation.warn "Product.distinct_by_product_ids is deprecated and should not be used"

      sort_column = sort_order.split(" ").first

      # Postgres will complain when using ordering by expressions not present in
      # SELECT DISTINCT. e.g.
      #
      #   PG::InvalidColumnReference: ERROR:  for SELECT DISTINCT, ORDER BY
      #   expressions must appear in select list. e.g.
      #
      #   SELECT  DISTINCT "spree_products".* FROM "spree_products" LEFT OUTER JOIN
      #   "spree_variants" ON "spree_variants"."product_id" = "spree_products"."id" AND "spree_variants"."is_master" = 't'
      #   AND "spree_variants"."deleted_at" IS NULL LEFT OUTER JOIN "spree_prices" ON
      #   "spree_prices"."variant_id" = "spree_variants"."id" AND "spree_prices"."currency" = 'USD'
      #   AND "spree_prices"."deleted_at" IS NULL WHERE "spree_products"."deleted_at" IS NULL AND ('t'='t')
      #   ORDER BY "spree_prices"."amount" ASC LIMIT 10 OFFSET 0
      #
      # Don't allow sort_column, a variable coming from params,
      # to be anything but a column in the database
      if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' && !column_names.include?(sort_column)
        all
      else
        distinct
      end
    end

    class << self
      private

      def price_table_name
        Spree::Price.quoted_table_name
      end

      # specifically avoid having an order for taxon search (conflicts with main order)
      def prepare_taxon_conditions(taxons)
        ids = taxons.map { |taxon| taxon.self_and_descendants.pluck(:id) }.flatten.uniq
        joins(:taxons).where("#{Spree::Taxon.table_name}.id" => ids)
      end

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

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

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