spree_core 4.5.3 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/app/finders/spree/option_values/find_available.rb +1 -1
  3. data/app/finders/spree/product_properties/find_available.rb +1 -1
  4. data/app/finders/spree/products/find.rb +20 -12
  5. data/app/finders/spree/taxons/find.rb +10 -7
  6. data/app/helpers/spree/base_helper.rb +2 -2
  7. data/app/models/concerns/spree/product_scopes.rb +27 -23
  8. data/app/models/concerns/spree/translatable_resource.rb +25 -0
  9. data/app/models/concerns/spree/translatable_resource_scopes.rb +24 -0
  10. data/app/models/concerns/spree/translatable_resource_slug.rb +17 -0
  11. data/app/models/spree/base.rb +1 -0
  12. data/app/models/spree/data_feed/google.rb +15 -0
  13. data/app/models/spree/data_feed.rb +40 -0
  14. data/app/models/spree/option_type.rb +4 -0
  15. data/app/models/spree/option_value.rb +4 -0
  16. data/app/models/spree/product.rb +41 -10
  17. data/app/models/spree/product_property.rb +12 -3
  18. data/app/models/spree/property.rb +7 -1
  19. data/app/models/spree/shipment.rb +2 -2
  20. data/app/models/spree/store.rb +20 -1
  21. data/app/models/spree/taxon.rb +22 -6
  22. data/app/models/spree/taxonomy.rb +4 -0
  23. data/app/models/spree/variant.rb +4 -7
  24. data/app/services/spree/data_feeds/google/optional_attributes.rb +23 -0
  25. data/app/services/spree/data_feeds/google/optional_sub_attributes.rb +21 -0
  26. data/app/services/spree/data_feeds/google/products_list.rb +14 -0
  27. data/app/services/spree/data_feeds/google/required_attributes.rb +67 -0
  28. data/app/services/spree/data_feeds/google/rss.rb +107 -0
  29. data/app/sorters/spree/products/sort.rb +23 -0
  30. data/brakeman.ignore +326 -18
  31. data/config/initializers/friendly_id.rb +2 -0
  32. data/config/initializers/mobility.rb +18 -0
  33. data/config/locales/en.yml +1 -0
  34. data/db/migrate/20220706112554_create_product_name_and_description_translations_for_mobility_table_backend.rb +27 -0
  35. data/db/migrate/20220715083542_create_spree_product_translations_for_mobility.rb +7 -0
  36. data/db/migrate/20220715120222_change_product_name_null_to_true.rb +5 -0
  37. data/db/migrate/20220718100743_create_spree_taxon_name_and_description_translations_for_mobility_table_backend.rb +27 -0
  38. data/db/migrate/20220718100948_change_taxon_name_null_to_true.rb +5 -0
  39. data/db/migrate/20220802070609_add_locale_to_friendly_id_slugs.rb +11 -0
  40. data/db/migrate/20220802073225_create_spree_product_slug_translations_for_mobility_table_backend.rb +5 -0
  41. data/db/migrate/20220804073928_transfer_data_to_translatable_tables.rb +66 -0
  42. data/db/migrate/20221215151408_add_selected_locale_to_spree_users.rb +8 -0
  43. data/db/migrate/20221219123957_add_deleted_at_to_product_translations.rb +6 -0
  44. data/db/migrate/20221220133432_add_uniqueness_constraint_to_product_translations.rb +5 -0
  45. data/db/migrate/20221229132350_create_spree_data_feed_settings.rb +14 -0
  46. data/db/migrate/20230103144439_create_option_type_translations.rb +26 -0
  47. data/db/migrate/20230103151034_create_option_value_translations.rb +26 -0
  48. data/db/migrate/20230109084253_create_product_property_translations.rb +25 -0
  49. data/db/migrate/20230109094907_transfer_options_data_to_translatable_tables.rb +58 -0
  50. data/db/migrate/20230109105943_create_property_translations.rb +26 -0
  51. data/db/migrate/20230109110840_transfer_property_data_to_translatable_tables.rb +59 -0
  52. data/db/migrate/20230110142344_backfill_friendly_id_slug_locale.rb +15 -0
  53. data/db/migrate/20230111121534_add_additional_taxon_translation_fields.rb +8 -0
  54. data/db/migrate/20230111122511_transfer_product_and_taxon_data_to_translatable_tables.rb +82 -0
  55. data/db/migrate/20230117115531_create_taxonomy_translations.rb +24 -0
  56. data/db/migrate/20230117120430_allow_null_taxonomy_name.rb +5 -0
  57. data/db/migrate/20230117121303_transfer_taxonomy_data_to_translatable_tables.rb +11 -0
  58. data/db/migrate/20230210142732_create_store_translations.rb +50 -0
  59. data/db/migrate/20230210142849_transfer_store_data_to_translatable_tables.rb +11 -0
  60. data/db/migrate/20230210230434_add_deleted_at_to_store_translations.rb +6 -0
  61. data/db/migrate/20230415155958_rename_data_feed_settings_table.rb +5 -0
  62. data/db/migrate/20230415160828_rename_data_feed_table_columns.rb +7 -0
  63. data/db/migrate/20230415161226_add_indexes_to_data_feeds_table.rb +5 -0
  64. data/db/migrate/20230512094803_rename_data_feeds_column_provider_to_type.rb +5 -0
  65. data/db/migrate/20230514162157_add_index_on_locale_and_permalink_to_spree_taxons.rb +5 -0
  66. data/lib/spree/core/configuration.rb +1 -0
  67. data/lib/spree/core/controller_helpers/locale.rb +26 -2
  68. data/lib/spree/core/dependencies.rb +70 -94
  69. data/lib/spree/core/dependencies_helper.rb +19 -0
  70. data/lib/spree/core/engine.rb +6 -1
  71. data/lib/spree/core/product_duplicator.rb +1 -1
  72. data/lib/spree/core/product_filters.rb +7 -4
  73. data/lib/spree/core/search/base.rb +1 -1
  74. data/lib/spree/core/version.rb +1 -1
  75. data/lib/spree/core.rb +2 -0
  76. data/lib/spree/permitted_attributes.rb +1 -1
  77. data/lib/spree/testing_support/factories/google_data_feed_factory.rb +8 -0
  78. data/lib/spree/testing_support/factories/product_factory.rb +6 -0
  79. data/lib/spree/testing_support/factories/product_translation_factory.rb +6 -0
  80. data/lib/spree/testing_support/factories/store_factory.rb +1 -0
  81. data/lib/spree/testing_support/factories/variant_factory.rb +4 -0
  82. data/lib/spree/translation_migrations.rb +40 -0
  83. data/spree_core.gemspec +3 -0
  84. metadata +92 -4
@@ -3,6 +3,8 @@ require 'stringex'
3
3
 
4
4
  module Spree
5
5
  class Taxon < Spree::Base
6
+ include TranslatableResource
7
+ include TranslatableResourceSlug
6
8
  include Metadata
7
9
  if defined?(Spree::Webhooks)
8
10
  include Spree::Webhooks::HasWebhooks
@@ -27,7 +29,7 @@ module Spree
27
29
  has_many :promotion_rule_taxons, class_name: 'Spree::PromotionRuleTaxon', dependent: :destroy
28
30
  has_many :promotion_rules, through: :promotion_rule_taxons, class_name: 'Spree::PromotionRule'
29
31
 
30
- validates :name, presence: true, uniqueness: { scope: [:parent_id, :taxonomy_id], allow_blank: true, case_sensitive: false }
32
+ validates :name, presence: true, uniqueness: { scope: [:parent_id, :taxonomy_id], case_sensitive: false }
31
33
  validates :taxonomy, presence: true
32
34
  validates :permalink, uniqueness: { case_sensitive: false, scope: [:parent_id, :taxonomy_id] }
33
35
  validates :hide_from_nav, inclusion: { in: [true, false] }
@@ -54,6 +56,24 @@ module Spree
54
56
 
55
57
  scope :for_stores, ->(stores) { joins(:taxonomy).where(spree_taxonomies: { store_id: stores.ids }) }
56
58
 
59
+ TRANSLATABLE_FIELDS = %i[name description permalink].freeze
60
+ translates(*TRANSLATABLE_FIELDS)
61
+
62
+ self::Translation.class_eval do
63
+ alias_attribute :slug, :permalink
64
+
65
+ before_create :set_permalink
66
+
67
+ def set_permalink
68
+ parent = translated_model.parent
69
+ if parent.present?
70
+ self.permalink = [parent.permalink, (permalink.blank? ? name.to_url : permalink.split('/').last)].join('/')
71
+ else
72
+ self.permalink = name.to_url if permalink.blank?
73
+ end
74
+ end
75
+ end
76
+
57
77
  # indicate which filters should be used for a taxon
58
78
  # this method should be customized to your own site
59
79
  def applicable_filters
@@ -73,11 +93,7 @@ module Spree
73
93
 
74
94
  # Creates permalink base for friendly_id
75
95
  def set_permalink
76
- if parent.present?
77
- self.permalink = [parent.permalink, (permalink.blank? ? name.to_url : permalink.split('/').last)].join('/')
78
- else
79
- self.permalink = name.to_url if permalink.blank?
80
- end
96
+ translations.each(&:set_permalink)
81
97
  end
82
98
 
83
99
  def active_products
@@ -1,10 +1,14 @@
1
1
  module Spree
2
2
  class Taxonomy < Spree::Base
3
+ include TranslatableResource
3
4
  include Metadata
4
5
  if defined?(Spree::Webhooks)
5
6
  include Spree::Webhooks::HasWebhooks
6
7
  end
7
8
 
9
+ TRANSLATABLE_FIELDS = %i[name].freeze
10
+ translates(*TRANSLATABLE_FIELDS)
11
+
8
12
  acts_as_list
9
13
 
10
14
  validates :name, presence: true, uniqueness: { case_sensitive: false, allow_blank: true, scope: :store_id }
@@ -122,16 +122,13 @@ module Spree
122
122
  self.whitelisted_ransackable_scopes = %i(product_name_or_sku_cont search_by_product_name_or_sku)
123
123
 
124
124
  def self.product_name_or_sku_cont(query)
125
- joins(:product).where("LOWER(#{Product.table_name}.name) LIKE LOWER(:query) OR LOWER(sku) LIKE LOWER(:query)", query: "%#{query}%")
125
+ joins(:product).join_translation_table(Product).
126
+ where("LOWER(#{Product.translation_table_alias}.name) LIKE LOWER(:query)
127
+ OR LOWER(sku) LIKE LOWER(:query)", query: "%#{query}%")
126
128
  end
127
129
 
128
130
  def self.search_by_product_name_or_sku(query)
129
- if defined?(SpreeGlobalize)
130
- joins(product: :translations).where("LOWER(#{Product::Translation.table_name}.name) LIKE LOWER(:query) OR LOWER(sku) LIKE LOWER(:query)",
131
- query: "%#{query}%")
132
- else
133
- product_name_or_sku_cont(query)
134
- end
131
+ product_name_or_sku_cont(query)
135
132
  end
136
133
 
137
134
  def available?
@@ -0,0 +1,23 @@
1
+ module Spree
2
+ module DataFeeds
3
+ module Google
4
+ class OptionalAttributes
5
+ prepend Spree::ServiceModule::Base
6
+
7
+ def call(input)
8
+ information = {}
9
+
10
+ input[:product].property_ids.each do |key|
11
+ name = Spree::Property.find(key)&.name
12
+ value = input[:product].property(name)
13
+ unless value.nil?
14
+ information[name] = value
15
+ end
16
+ end
17
+
18
+ success(information: information)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module Spree
2
+ module DataFeeds
3
+ module Google
4
+ class OptionalSubAttributes
5
+ prepend Spree::ServiceModule::Base
6
+
7
+ def call(input)
8
+ information = {}
9
+
10
+ # This is a place where you can put attributes that have sub-attributes, example for shipping:
11
+ #
12
+ # information['shipping'] = {}
13
+ # information['shipping']['price'] = calculate_shipping(input[:product])
14
+ # information['shipping']['country'] = input[:store].default_country
15
+
16
+ success(information: information)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module Spree
2
+ module DataFeeds
3
+ module Google
4
+ class ProductsList
5
+ prepend Spree::ServiceModule::Base
6
+
7
+ def call(store)
8
+ products = store.products.active
9
+ success(products: products)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,67 @@
1
+ module Spree
2
+ module DataFeeds
3
+ module Google
4
+ class RequiredAttributes
5
+ prepend Spree::ServiceModule::Base
6
+
7
+ def call(input)
8
+ information = {}
9
+
10
+ return failure(nil, error: 'No image link') if get_image_link(input[:variant], input[:product]).nil?
11
+
12
+ information['id'] = input[:variant].id
13
+ information['title'] = format_title(input[:product], input[:variant])
14
+ information['description'] = get_description(input[:product], input[:variant])
15
+ information['link'] = "#{input[:store].url}/#{input[:product].slug}"
16
+ information['image_link'] = get_image_link(input[:variant], input[:product])
17
+ information['price'] = format_price(input[:variant])
18
+ information['availability'] = get_availability(input[:product])
19
+ information['availability_date'] = input[:product].available_on&.xmlschema unless input[:product].available_on.nil?
20
+
21
+ success(information: information)
22
+ end
23
+
24
+ private
25
+
26
+ def format_title(product, variant)
27
+ # Title of a variant is created by joining title of a product and variant's option_values, as they are
28
+ # what differentiates it from other variants.
29
+ title = product.name
30
+ variant.option_values.find_each do |option_value|
31
+ title << " - #{option_value.name}"
32
+ end
33
+ title
34
+ end
35
+
36
+ def get_description(product, variant)
37
+ return product.description unless product.description.nil?
38
+
39
+ format_title(product, variant)
40
+ end
41
+
42
+ def get_image_link(variant, product)
43
+ # try getting image from variant
44
+ img = variant.images.first&.plp_url
45
+
46
+ # if no image specified for variant try getting product image
47
+ if img.nil?
48
+ img = product.images.first&.plp_url
49
+ end
50
+
51
+ img
52
+ end
53
+
54
+ def format_price(variant)
55
+ "#{variant.price} #{variant.cost_currency}"
56
+ end
57
+
58
+ def get_availability(product)
59
+ return 'in stock' if product.available? && product.available_on&.past?
60
+ return 'backorder' if product.backorderable? && product.backordered? && product.available_on&.future?
61
+
62
+ 'out of stock'
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,107 @@
1
+ require 'nokogiri'
2
+
3
+ module Spree
4
+ module DataFeeds
5
+ module Google
6
+ class Rss
7
+ prepend Spree::ServiceModule::Base
8
+
9
+ def call(settings)
10
+ @settings = settings
11
+
12
+ return failure(store, error: "Store with id: #{settings.store_id} does not exist.") if store.nil?
13
+
14
+ builder = Nokogiri::XML::Builder.new do |xml|
15
+ xml.rss('xmlns:g' => 'http://base.google.com/ns/1.0', 'version' => '2.0') do
16
+ xml.channel do
17
+ add_store_information_to_xml(xml)
18
+ result = products_list.call(store)
19
+ if result.success?
20
+ result.value[:products].find_each do |product|
21
+ product.variants.active.find_each do |variant|
22
+ add_variant_information_to_xml(xml, product, variant)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ success(file: builder.to_xml)
31
+ end
32
+
33
+ private
34
+
35
+ def store
36
+ return @store if defined? @store
37
+
38
+ @store ||= Spree::Store.find_by(id: @settings.store_id)
39
+ end
40
+
41
+ def add_store_information_to_xml(xml)
42
+ xml.title store.name
43
+ xml.link store.url
44
+ xml.description store.meta_description
45
+ end
46
+
47
+ def add_variant_information_to_xml(xml, product, variant)
48
+ input = { product: product, variant: variant, settings: @settings, store: store }
49
+ result = required_attributes.call(input)
50
+
51
+ if result.success
52
+ xml.item do
53
+ result.value[:information]&.each do |key, value|
54
+ xml['g'].send(key, value)
55
+ end
56
+
57
+ add_optional_information(xml, product, variant)
58
+ add_optional_sub_attributes(xml, product, variant)
59
+ end
60
+ end
61
+ end
62
+
63
+ def add_optional_information(xml, product, variant)
64
+ input = { product: product, variant: variant, settings: @settings, store: store }
65
+ result = optional_attributes.call(input)
66
+ if result.success?
67
+ information = result.value[:information]
68
+ information.each do |key, value|
69
+ xml['g'].send(key, value)
70
+ end
71
+ end
72
+ end
73
+
74
+ def add_optional_sub_attributes(xml, product, variant)
75
+ input = { product: product, variant: variant, settings: @settings, store: store }
76
+ result = optional_sub_attributes.call(input)
77
+ if result.success?
78
+ information = result.value[:information]
79
+ information.each do |key, value|
80
+ xml['g'].send(key) do
81
+ value.each do |subkey, subvalue|
82
+ xml['g'].send(subkey, subvalue)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def optional_attributes
90
+ Spree::Dependencies.data_feeds_google_optional_attributes_service.constantize.new
91
+ end
92
+
93
+ def required_attributes
94
+ Spree::Dependencies.data_feeds_google_required_attributes_service.constantize.new
95
+ end
96
+
97
+ def optional_sub_attributes
98
+ Spree::Dependencies.data_feeds_google_optional_sub_attributes_service.constantize.new
99
+ end
100
+
101
+ def products_list
102
+ Spree::Dependencies.data_feeds_google_products_list.constantize.new
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -11,6 +11,8 @@ module Spree
11
11
  products = by_price(products)
12
12
  products = by_sku(products)
13
13
 
14
+ products = select_translatable_fields(products)
15
+
14
16
  products.distinct
15
17
  end
16
18
 
@@ -46,6 +48,27 @@ module Spree
46
48
  def sort_by?(field)
47
49
  sort.detect { |s| s[0] == field }
48
50
  end
51
+
52
+ # Add translatable fields to SELECT statement to avoid InvalidColumnReference error (workaround for Mobility issue #596)
53
+ def select_translatable_fields(scope)
54
+ translatable_fields = translatable_sortable_fields
55
+ return scope if translatable_fields.empty?
56
+
57
+ # if sorting by 'sku' or 'price', spree_products.* is already included in SELECT statement
58
+ if sort_by?('sku') || sort_by?('price')
59
+ scope.i18n.select(*translatable_fields)
60
+ else
61
+ scope.i18n.select("#{Product.table_name}.*").select(*translatable_fields)
62
+ end
63
+ end
64
+
65
+ def translatable_sortable_fields
66
+ fields = []
67
+ Product.translatable_fields.each do |field|
68
+ fields << field if sort_by?(field.to_s)
69
+ end
70
+ fields
71
+ end
49
72
  end
50
73
  end
51
74
  end