diff --git a/.gitignore b/.gitignore index 338a114..b852a21 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ tmp nbproject *.swp .bundle +spec/dummy diff --git a/Gemfile b/Gemfile index 65bb50b..3378010 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ group :development, :test do gem 'poltergeist', '~> 1.5' end -gem 'spree', '2.2.0' -gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '2-2-stable' +gem 'spree', '~> 2.4.0' +gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: '2-4-stable' gemspec diff --git a/Gemfile.lock b/Gemfile.lock index 0bb13da..beefc3c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,83 +1,76 @@ GIT remote: git://github.com/spree/spree_auth_devise.git - revision: 1ccd7048bf5595bcfd0ccdb7a8863c12fcf84991 - branch: 2-2-stable + revision: 997382fcb1f6ed78a201ee7016384cfe71745aea + branch: 2-4-stable specs: spree_auth_devise (2.2.0) - cancan (~> 1.6.10) devise (~> 3.2.3) devise-encryptable (= 0.1.2) json multi_json - spree_backend (~> 2.2.0) - spree_core (~> 2.2.0) - spree_frontend (~> 2.2.0) + spree_core (~> 2.4.0) PATH remote: . specs: - spree_google_merchant (1.1.0) + spree_google_merchant (1.3.0.8.alpha) spree_api spree_backend - spree_core (~> 2.2) + spree_core (~> 2.4) GEM remote: https://rubygems.org/ specs: - actionmailer (4.0.3) - actionpack (= 4.0.3) - mail (~> 2.5.4) - actionpack (4.0.3) - activesupport (= 4.0.3) - builder (~> 3.1.0) - erubis (~> 2.7.0) + actionmailer (4.1.9) + actionpack (= 4.1.9) + actionview (= 4.1.9) + mail (~> 2.5, >= 2.5.4) + actionpack (4.1.9) + actionview (= 4.1.9) + activesupport (= 4.1.9) rack (~> 1.5.2) rack-test (~> 0.6.2) - active_utils (2.0.2) + actionview (4.1.9) + activesupport (= 4.1.9) + builder (~> 3.1) + erubis (~> 2.7.0) + active_utils (2.2.3) activesupport (>= 2.3.11) i18n - activemerchant (1.42.6) - active_utils (~> 2.0, >= 2.0.1) - activesupport (>= 2.3.14, < 5.0.0) + activemerchant (1.44.1) + active_utils (~> 2.2.0) + activesupport (>= 3.2.14, < 5.0.0) builder (>= 2.1.2, < 4.0.0) - i18n (~> 0.5) + i18n (>= 0.6.9) json (~> 1.7) - money (< 7.0.0) nokogiri (~> 1.4) - activemodel (4.0.3) - activesupport (= 4.0.3) - builder (~> 3.1.0) - activerecord (4.0.3) - activemodel (= 4.0.3) - activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.3) - arel (~> 4.0.0) - activerecord-deprecated_finders (1.0.3) - activesupport (4.0.3) - i18n (~> 0.6, >= 0.6.4) - minitest (~> 4.2) - multi_json (~> 1.3) + offsite_payments (~> 2.0.0) + activemodel (4.1.9) + activesupport (= 4.1.9) + builder (~> 3.1) + activerecord (4.1.9) + activemodel (= 4.1.9) + activesupport (= 4.1.9) + arel (~> 5.0.0) + activesupport (4.1.9) + i18n (~> 0.6, >= 0.6.9) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) thread_safe (~> 0.1) - tzinfo (~> 0.3.37) - acts_as_list (0.3.0) + tzinfo (~> 1.1) + acts_as_list (0.6.0) activerecord (>= 3.0) addressable (2.3.5) - arel (4.0.2) - atomic (1.1.15) - awesome_nested_set (3.0.0.rc.3) + arel (5.0.1.20140414130214) + awesome_nested_set (3.0.2) activerecord (>= 4.0.0, < 5) - aws-sdk (1.27.0) - json (~> 1.4) - nokogiri (>= 1.4.4) - uuidtools (~> 2.1) - bcrypt (3.1.7) - bcrypt-ruby (3.1.5) - bcrypt (>= 3.1.3) + bcrypt (3.1.10) bourne (1.5.0) mocha (>= 0.13.2, < 0.15) - builder (3.1.4) - cancan (1.6.10) - canonical-rails (0.0.3) + builder (3.2.2) + camertron-eprun (1.1.0) + cancancan (1.9.2) + canonical-rails (0.0.8) rails (>= 3.1, < 5.0) capybara (2.1.0) mime-types (>= 1.16) @@ -85,10 +78,13 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + carmen (1.0.2) + activesupport (>= 3.0.0) + cldr-plurals-runtime-rb (1.0.0) climate_control (0.0.3) activesupport (>= 3.0) cliver (0.3.2) - cocaine (0.5.3) + cocaine (0.5.7) climate_control (>= 0.0.3, < 1.0) coderay (1.1.0) coffee-rails (4.0.1) @@ -98,14 +94,17 @@ GEM coffee-script-source execjs coffee-script-source (1.7.0) - colorize (0.6.0) + colorize (0.7.5) + css_parser (1.3.6) + addressable database_cleaner (1.0.1) - deface (1.0.0) + deface (1.0.1) colorize (>= 0.5.8) nokogiri (~> 1.6.0) + polyglot rails (>= 3.1) - devise (3.2.3) - bcrypt-ruby (~> 3.0) + devise (3.2.4) + bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 3.2.6, < 5) thread_safe (~> 0.1) @@ -124,87 +123,106 @@ GEM factory_girl (~> 4.2.0) railties (>= 3.0.0) ffaker (1.23.0) - friendly_id (5.0.3) + font-awesome-rails (4.3.0.0) + railties (>= 3.2, < 5.0) + friendly_id (5.0.5) activerecord (>= 4.0.0) highline (1.6.21) hike (1.2.3) - httparty (0.13.0) + htmlentities (4.3.3) + httparty (0.13.3) json (~> 1.8) multi_xml (>= 0.5.2) - i18n (0.6.9) - jquery-rails (3.1.0) + i18n (0.7.0) + jquery-rails (3.1.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (4.1.2) - railties (>= 3.1.0) - json (1.8.1) - kaminari (0.15.1) + jquery-ui-rails (5.0.3) + railties (>= 3.2.16) + json (1.8.2) + kaminari (0.16.3) actionpack (>= 3.0.0) activesupport (>= 3.0.0) launchy (2.4.2) addressable (~> 2.3) - mail (2.5.4) - mime-types (~> 1.16) - treetop (~> 1.4.8) + mail (2.6.3) + mime-types (>= 1.16, < 3) metaclass (0.0.4) method_source (0.8.2) - mime-types (1.25.1) + mime-types (2.4.3) mini_portile (0.5.2) - minitest (4.7.5) + minitest (5.5.1) mocha (0.14.0) metaclass (~> 0.0.1) - monetize (0.1.4) - money (6.0.1) - i18n (~> 0.6.4) - monetize (~> 0.1.3) - multi_json (1.8.4) + monetize (1.1.0) + money (~> 6.5.0) + money (6.5.1) + i18n (>= 0.6.4, <= 0.7.0) + multi_json (1.11.0) multi_xml (0.5.5) nokogiri (1.6.1) mini_portile (~> 0.5.0) + offsite_payments (2.0.1) + active_utils (~> 2.2.0) + activesupport (>= 3.2.14, < 5.0.0) + builder (>= 2.1.2, < 4.0.0) + i18n (~> 0.5) + json (~> 1.7) + money (< 7.0.0) + nokogiri (~> 1.4) orm_adapter (0.5.0) - paperclip (3.4.2) + paperclip (4.2.1) activemodel (>= 3.0.0) - activerecord (>= 3.0.0) activesupport (>= 3.0.0) - cocaine (~> 0.5.0) + cocaine (~> 0.5.3) mime-types - paranoia (2.0.2) + paranoia (2.0.5) activerecord (~> 4.0) poltergeist (1.5.0) capybara (~> 2.1) cliver (~> 0.3.1) multi_json (~> 1.0) websocket-driver (>= 0.2.0) - polyamorous (0.6.4) + polyamorous (1.1.0) activerecord (>= 3.0) - polyglot (0.3.4) + polyglot (0.3.5) + premailer (1.8.4) + css_parser (>= 1.3.6) + htmlentities (>= 4.0.0) + premailer-rails (1.8.0) + actionmailer (>= 3, < 5) + premailer (~> 1.7, >= 1.7.9) pry (0.9.12.6) coderay (~> 1.0) method_source (~> 0.8) slop (~> 3.4) - rabl (0.9.3) + rabl (0.9.4.pre1) activesupport (>= 2.3.14) rack (1.5.2) - rack-test (0.6.2) + rack-test (0.6.3) rack (>= 1.0) - rails (4.0.3) - actionmailer (= 4.0.3) - actionpack (= 4.0.3) - activerecord (= 4.0.3) - activesupport (= 4.0.3) + rails (4.1.9) + actionmailer (= 4.1.9) + actionpack (= 4.1.9) + actionview (= 4.1.9) + activemodel (= 4.1.9) + activerecord (= 4.1.9) + activesupport (= 4.1.9) bundler (>= 1.3.0, < 2.0) - railties (= 4.0.3) - sprockets-rails (~> 2.0.0) - railties (4.0.3) - actionpack (= 4.0.3) - activesupport (= 4.0.3) + railties (= 4.1.9) + sprockets-rails (~> 2.0) + railties (4.1.9) + actionpack (= 4.1.9) + activesupport (= 4.1.9) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.1.1) - ransack (1.1.0) + rake (10.4.2) + ransack (1.4.1) actionpack (>= 3.0) activerecord (>= 3.0) - polyamorous (~> 0.6.0) + activesupport (>= 3.0) + i18n + polyamorous (~> 1.1) rspec-core (2.14.8) rspec-expectations (2.14.5) diff-lcs (>= 1.1.3, < 2.0) @@ -217,66 +235,69 @@ GEM rspec-core (~> 2.14.0) rspec-expectations (~> 2.14.0) rspec-mocks (~> 2.14.0) - sass (3.2.14) - sass-rails (4.0.1) + sass (3.2.19) + sass-rails (4.0.5) railties (>= 4.0.0, < 5.0) - sass (>= 3.1.10) - sprockets-rails (~> 2.0.0) - select2-rails (3.4.9) - sass-rails + sass (~> 3.2.2) + sprockets (~> 2.8, < 3.0) + sprockets-rails (~> 2.0) + select2-rails (3.5.9.1) thor (~> 0.14) shoulda-matchers (1.5.6) activesupport (>= 3.0.0) bourne (~> 1.3) slop (3.4.7) - spree (2.2.0) - spree_api (= 2.2.0) - spree_backend (= 2.2.0) - spree_cmd (= 2.2.0) - spree_core (= 2.2.0) - spree_frontend (= 2.2.0) - spree_sample (= 2.2.0) - spree_api (2.2.0) - rabl (= 0.9.3) - spree_core (= 2.2.0) - versioncake (~> 1.2.0) - spree_backend (2.2.0) - jquery-rails (~> 3.1.0) - jquery-ui-rails (~> 4.1.0) - select2-rails (~> 3.4.7) - spree_api (= 2.2.0) - spree_core (= 2.2.0) - spree_cmd (2.2.0) + spree (2.4.6) + spree_api (= 2.4.6) + spree_backend (= 2.4.6) + spree_cmd (= 2.4.6) + spree_core (= 2.4.6) + spree_frontend (= 2.4.6) + spree_sample (= 2.4.6) + spree_api (2.4.6) + rabl (~> 0.9.4.pre1) + spree_core (= 2.4.6) + versioncake (~> 2.3.1) + spree_backend (2.4.6) + jquery-rails (~> 3.1.2) + jquery-ui-rails (~> 5.0.0) + select2-rails (= 3.5.9.1) + spree_api (= 2.4.6) + spree_core (= 2.4.6) + spree_cmd (2.4.6) thor (~> 0.14) - spree_core (2.2.0) - activemerchant (~> 1.42.3) - acts_as_list (= 0.3.0) - awesome_nested_set (~> 3.0.0.rc.3) - aws-sdk (= 1.27.0) - cancan (~> 1.6.10) + spree_core (2.4.6) + activemerchant (~> 1.44.1) + acts_as_list (~> 0.3) + awesome_nested_set (~> 3.0.1) + cancancan (~> 1.9.2) + carmen (~> 1.0.0) deface (~> 1.0.0) ffaker (~> 1.16) - friendly_id (= 5.0.3) + font-awesome-rails (~> 4.0) + friendly_id (~> 5.0.4) highline (~> 1.6.18) httparty (~> 0.11) json (~> 1.7) - kaminari (~> 0.15.0) - paperclip (~> 3.4.1) - paranoia (~> 2.0) - rails (~> 4.0.3) - ransack (~> 1.1.0) + kaminari (~> 0.15, >= 0.15.1) + monetize (~> 1.1) + paperclip (~> 4.2.0) + paranoia (~> 2.0.5) + premailer-rails + rails (~> 4.1.8) + ransack (~> 1.4.1) state_machine (= 1.2.0) stringex (~> 1.5.1) truncate_html (= 0.9.2) - spree_frontend (2.2.0) - canonical-rails - jquery-rails (~> 3.1.0) - spree_api (= 2.2.0) - spree_core (= 2.2.0) - stringex (~> 1.5.1) - spree_sample (2.2.0) - spree_core (= 2.2.0) - sprockets (2.11.0) + twitter_cldr (~> 3.0) + spree_frontend (2.4.6) + canonical-rails (~> 0.0.4) + jquery-rails (~> 3.1.2) + spree_api (= 2.4.6) + spree_core (= 2.4.6) + spree_sample (2.4.6) + spree_core (= 2.4.6) + sprockets (2.12.3) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) @@ -288,17 +309,18 @@ GEM sqlite3 (1.3.9) state_machine (1.2.0) stringex (1.5.1) - thor (0.18.1) - thread_safe (0.2.0) - atomic (>= 1.1.7, < 2) + thor (0.19.1) + thread_safe (0.3.5) tilt (1.4.1) - treetop (1.4.15) - polyglot - polyglot (>= 0.3.1) truncate_html (0.9.2) - tzinfo (0.3.38) - uuidtools (2.1.4) - versioncake (1.2.0) + twitter_cldr (3.1.2) + camertron-eprun + cldr-plurals-runtime-rb (~> 1.0.0) + json + tzinfo + tzinfo (1.2.2) + thread_safe (~> 0.1) + versioncake (2.3.1) actionpack (>= 3.2) activesupport (>= 3.2) railties (>= 3.2) @@ -324,9 +346,9 @@ DEPENDENCIES pry rails (~> 4) rspec-rails (~> 2.13) - sass-rails (~> 4.0.0) + sass-rails (~> 4.0.2) shoulda-matchers (~> 1.5) - spree (= 2.2.0) + spree (~> 2.4.0) spree_auth_devise! spree_google_merchant! sqlite3 diff --git a/README.md b/README.md index 71475b2..d70c8c4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Summary [![Code Climate](https://codeclimate.com/repos/5313bdf0695680405a00c039/badges/cd37fe1b53bc4d556c29/gpa.png)](https://codeclimate.com/repos/5313bdf0695680405a00c039/feed) -[![Build Status](https://travis-ci.org/Lordnibbler/spree_google_merchant.png?branch=2-2-stable)](https://travis-ci.org/Lordnibbler/spree_google_merchant) +[![Build Status](https://travis-ci.org/Lordnibbler/spree_google_merchant.png?branch=2-4-stable)](https://travis-ci.org/Lordnibbler/spree_google_merchant) Provides an up-to-date RSS product feed for Google Merchant rather a file that you have to upload. This is a very basic extension so feel free to help improve it! @@ -22,6 +22,14 @@ Then bundle bundle install ``` +Then (if you wish to customize the feed), install the config/initializers/google_merchant.rb file: + +```ruby +rails g spree_google_merchant:install +``` + +You can view the initializer to get an idea how to customize things. + Next, configure the feed title, description and site URL by browsing to the Google Merchant settings page in `Admin -> Configuration` Finally, set up your products in Spree by editing the product's properties. You should add the following properties: diff --git a/app/controllers/spree/products_controller_decorator.rb b/app/controllers/spree/products_controller_decorator.rb index 3cf8f5b..ad79cbe 100644 --- a/app/controllers/spree/products_controller_decorator.rb +++ b/app/controllers/spree/products_controller_decorator.rb @@ -1,7 +1,57 @@ module Spree ProductsController.class_eval do + helper_method :manager, :include_variants?, :in_stock, :properties + def google_merchant - @products = Product.active + if include_variants? + @items = full_variants + @properties = GoogleMerchant::Manager.property_set( + @items.collect(&:product).uniq.sort_by(&:id) + ) + else + @items = Product.active.includes(:variants) + @properties = GoogleMerchant::Manager.property_set(@items) + end + respond_to do |format| + format.any(:xml, :rss) { render formats: [:xml, :rss] } + end + end + + private + + def full_variants + t1 = Variant.arel_table + reflections = HashWithIndifferentAccess.new(Variant.reflections) + t2_name = "#{reflections[:first_non_master].plural_name}_#{t1.name}" + t2 = Arel::Table.new(t2_name) + + Variant.active.where( + t1[:is_master].eq(false).or(t2[:id].eq(nil)) + ).includes( + :product, + :advertising_image, + :default_price, + :stock_items, + :master, + :first_non_master, + {option_values: :option_type} + ).references(t2_name) + end + + def manager + @manager ||= GoogleMerchant::Manager + end + + def include_variants? + manager.include_variants? + end + + def in_stock + @in_stock ||= Variant.in_stock.pluck(:id) + end + + def properties + @properties end end end diff --git a/app/helpers/spree/products_helper_decorator.rb b/app/helpers/spree/products_helper_decorator.rb new file mode 100644 index 0000000..6301e2b --- /dev/null +++ b/app/helpers/spree/products_helper_decorator.rb @@ -0,0 +1,30 @@ +module Spree + ProductsHelper.class_eval do + def google_merchant_tag(xml, key, value) + return unless value.present? + if value.respond_to?(:keys) + xml.tag!("g:#{key}") { nested_google_merchant_tag(xml, value) } + else + xml.tag! "g:#{key}", value + end + end + + def nested_google_merchant_tag(xml, group) + group.each do |sub_key, sub_value| + google_merchant_tag(xml, sub_key, sub_value) + end + end + + def feed_title + manager.feed_title + end + + def feed_description + manager.feed_description + end + + def production_domain + manager.production_domain + end + end +end diff --git a/app/models/spree/product_property_decorator.rb b/app/models/spree/product_property_decorator.rb new file mode 100644 index 0000000..012a60c --- /dev/null +++ b/app/models/spree/product_property_decorator.rb @@ -0,0 +1,23 @@ +module Spree + ProductProperty.class_eval do + scope :google_properties, ->{ + select( + "#{table_name}.*, #{property_table_name}.name" + ).joins( + :property + ).where( + property_id: google_merchant_property_ids + ) + } + + private + + def self.property_table_name + Property.table_name + end + + def self.google_merchant_property_ids + GoogleMerchant::Manager.property_ids + end + end +end diff --git a/app/models/spree/variant_decorator.rb b/app/models/spree/variant_decorator.rb new file mode 100644 index 0000000..1536d6e --- /dev/null +++ b/app/models/spree/variant_decorator.rb @@ -0,0 +1,16 @@ +module Spree + Variant.class_eval do + has_one( + :advertising_image, + Spree::GoogleMerchant::Manager.advertising_image_proc, + as: :viewable, + class_name: "Spree::Image" + ) + has_one :master, ->{ where(is_master: true) }, primary_key: :product_id, foreign_key: :product_id, class_name: "Spree::Variant" + has_one :first_non_master, ->{ where(is_master: false).order(:id) }, primary_key: :product_id, foreign_key: :product_id, class_name: "Spree::Variant" + + def advertising_image_url + advertising_image.try(:attachment).try(:url, :product) + end + end +end diff --git a/app/views/spree/products/_product_attributes.rss.builder b/app/views/spree/products/_product_attributes.rss.builder index 08dea74..afc6b97 100644 --- a/app/views/spree/products/_product_attributes.rss.builder +++ b/app/views/spree/products/_product_attributes.rss.builder @@ -1,30 +1,16 @@ # docs: https://support.google.com/merchants/answer/188494?hl=en -google_merchant_product_category = Spree::Property.where(name: "google_merchant_product_category").first -google_merchant_brand = Spree::Property.where(name: "google_merchant_brand").first -google_merchant_department = Spree::Property.where(name: "google_merchant_department").first -google_merchant_color = Spree::Property.where(name: "google_merchant_color").first -google_merchant_gtin = Spree::Property.where(name: "google_merchant_gtin").first - -category = variant.product.product_properties.where(property_id: google_merchant_product_category.id).first if google_merchant_product_category -brand = variant.product.product_properties.where(property_id: google_merchant_brand.id).first if google_merchant_brand -department = variant.product.product_properties.where(property_id: google_merchant_department.id).first if google_merchant_department -color = variant.product.product_properties.where(property_id: google_merchant_color.id).first if google_merchant_color -gtin = variant.product.product_properties.where(property_id: google_merchant_gtin.id).first if google_merchant_gtin - -xml.title "#{variant.product.name} #{variant_options variant}" -xml.description variant.product.description -xml.link @production_domain + 'products/' + variant.product.slug -xml.tag! "sku", variant.sku.to_s -xml.tag! "brand", brand.value if brand -xml.tag! "department", department.value if department -xml.tag! "image", variant.product.images.first.attachment.url(:product) unless variant.product.images.empty? -xml.tag! "color", color.value if color -xml.tag! "GTIN", gtin.value if gtin -xml.tag! "g:price", variant.price -xml.tag! "g:google_product_category", category.value if category -xml.tag! "g:product_type", category.value if category xml.tag! "g:id", variant.sku.to_s +xml.tag! "g:title", manager.title(variant) +xml.tag! "g:description", variant.product.description +xml.tag! "g:link", production_domain + manager.variant_path(variant) +xml.tag! "g:image_link", variant.advertising_image_url.gsub(/^\//, 'http:/') if variant.advertising_image xml.tag! "g:condition", "new" -xml.tag! "g:availability", Spree::Stock::Quantifier.new(variant).total_on_hand > 0 ? 'in stock' : 'out of stock' -xml.tag! "shipping_weight", variant.weight.to_s +xml.tag! "g:availability", in_stock.include?(variant.id) ? 'in stock' : 'out of stock' +xml.tag! "g:price", "#{variant.default_price.display_amount.money} #{variant.default_price.currency}" +xml.tag! "g:mpn", variant.sku.to_s +xml.tag! "g:shipping_weight", "#{variant.weight} #{manager.weight_unit}" +xml.tag! "g:item_group_id", variant.master.sku.to_s if include_variants? +properties[variant.product].merge(manager.option_values_for(variant)).each do |key, value| + google_merchant_tag(xml, key, value) +end diff --git a/app/views/spree/products/google_merchant.rss.builder b/app/views/spree/products/google_merchant.rss.builder index 4a289a2..869afa4 100644 --- a/app/views/spree/products/google_merchant.rss.builder +++ b/app/views/spree/products/google_merchant.rss.builder @@ -2,24 +2,16 @@ xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8" xml.rss "version" => "2.0", "xmlns:g" => "http://base.google.com/ns/1.0" do xml.channel do - xml.title Spree::GoogleMerchant::Config[:google_merchant_title] - xml.description Spree::GoogleMerchant::Config[:google_merchant_description] + xml.title feed_title + xml.description feed_description - @production_domain = Spree::GoogleMerchant::Config[:production_domain] - xml.link @production_domain + xml.link production_domain - @products.each do |product| - # if product.google_merchant_include_variants - # product.variants.each do |variant| - # xml.item do - # xml << render(:partial => 'product_attributes', :locals => { :variant => variant }) - # end - # end - # else - xml.item do - xml << render(:partial => 'product_attributes', :locals => { :variant => product.master }) - end - # end + @items.each do |item| + locals = { variant: (include_variants? ? item : item.master) } + xml.item do + xml << render(partial: 'product_attributes', locals: locals) + end end end end diff --git a/lib/generators/spree_google_merchant/install/install_generator.rb b/lib/generators/spree_google_merchant/install/install_generator.rb new file mode 100644 index 0000000..ee2e978 --- /dev/null +++ b/lib/generators/spree_google_merchant/install/install_generator.rb @@ -0,0 +1,73 @@ +module SpreeGoogleMerchant + module Generators + class InstallGenerator < Rails::Generators::Base + + def add_initializer + create_file "config/initializers/google_merchant.rb", <<-FILE +# Override the manager: +# module Spree +# module GoogleMerchant +# class CustomProductManager < ProductManager +# # Override methods: +# def variant_path(variant) +# 'products/' + variant.product.slug +# end +# +# . +# . +# . +# +# end +# +# send(:remove_const, :Manager) +# Manager = CustomProductManager.new +# end +# end + + +Spree::GoogleMerchant::Manager.tap do |config| + # config.include_variants = false + + # Replace/add to/remove from product_mapping. + # default is { + # google_merchant_product_category: "google_product_category", + # google_merchant_brand: "brand", + # google_merchant_department: "department", + # google_merchant_color: "color", + # google_merchant_gtin: "gtin" + # } + + # config.product_mapping.delete(:google_merchant_product_category) + + # config.product_mapping.merge!( + # material: "material" + # ) + + # Set a variant_mapping. + # config.variant_mapping = { + # 'my-color' => 'color', + # . + # . + # . + # } + + # Set fallback values for values that don't change across products. + # config.product_fallbacks = { + # brand: 'My Brand', + # . + # . + # . + # } + + # Customize what's considered "the first image". + # config.advertising_image_proc = ->{ where().order(:position) } + + # Set the units used for weight. Valid values are: lb, oz, g, or kg + # config.weight_unit = 'lb' +end + FILE + end + + end + end +end diff --git a/lib/spree/google_merchant/product_manager.rb b/lib/spree/google_merchant/product_manager.rb new file mode 100644 index 0000000..6cdb030 --- /dev/null +++ b/lib/spree/google_merchant/product_manager.rb @@ -0,0 +1,116 @@ +module Spree + module GoogleMerchant + class ProductManager + include Spree::BaseHelper + + DEFAULT_MAPPING = { + google_merchant_product_category: "google_product_category", + google_merchant_brand: "brand", + google_merchant_department: "department", + google_merchant_color: "color", + google_merchant_gtin: "gtin" + } + + VALID_WEIGHT_UNITS = %w(lb oz g kg) + + attr_accessor :product_fallbacks, :advertising_image_proc, :weight_unit + attr_writer :include_variants + attr_reader :product_mapping, :variant_mapping + + def initialize + @product_mapping = HashWithIndifferentAccess.new(DEFAULT_MAPPING) + @product_fallbacks = {} + @variant_mapping = {} + @include_variants = false + @advertising_image_proc = ->{order(:position)} + @weight_unit = VALID_WEIGHT_UNITS.first + end + + def product_mapping=(hash) + @product_mapping = HashWithIndifferentAccess.new(hash) + end + + def variant_mapping=(hash) + @variant_mapping = HashWithIndifferentAccess.new(hash) + end + + def product_keys + product_mapping.keys + end + + def variant_keys + variant_mapping.keys + end + + def include_variants? + @include_variants.present? + end + + def property_ids + Spree::Property.where(name: product_keys) + end + + def property_set(products) + lookup_properties + products.inject(HashWithIndifferentAccess.new) do |set, product| + set.merge(product => properties_for(product)) + end + end + + def option_values_for(variant) + all_values = options_for(variant).inject({}) do |x,k| + x.merge(k.option_type.name => k) + end + variant_keys.inject(HashWithIndifferentAccess.new) do |values, key| + value = all_values[key].try(:presentation) + value ? values.merge(variant_mapping[key] => value) : values + end + end + + def variant_path(variant) + 'products/' + variant.product.slug + end + + def title(variant) + "#{variant.product.name} #{variant_options variant}" + end + + def feed_title + Spree::GoogleMerchant::Config[:google_merchant_title] + end + + def feed_description + Spree::GoogleMerchant::Config[:google_merchant_description] + end + + def production_domain + Spree::GoogleMerchant::Config[:production_domain] + end + + private + + attr_reader :properties + + def lookup_properties + @properties = Spree::ProductProperty.google_properties.inject( + Hash.new { [] } + ) do |set, pp| + id = pp.product_id + set.merge(id => set[id] << pp) + end + end + + def properties_for(product) + properties[product.id].inject( + HashWithIndifferentAccess.new(product_fallbacks) + ) do |set, pp| + set.merge(product_mapping[pp.name] => pp.value) + end + end + + def options_for(variant) + variant.option_values.select { |ov| ov.option_type.name.in?(variant_keys) } + end + end + end +end diff --git a/lib/spree_google_merchant.rb b/lib/spree_google_merchant.rb index d11f134..5e1e2c6 100644 --- a/lib/spree_google_merchant.rb +++ b/lib/spree_google_merchant.rb @@ -10,6 +10,7 @@ class Engine < Rails::Engine initializer "spree.google_merchant.preferences", :before => :load_config_initializers do |app| Spree::GoogleMerchant::Config = Spree::GoogleMerchantConfiguration.new + Spree::GoogleMerchant::Manager = Spree::GoogleMerchant::ProductManager.new end def self.activate diff --git a/spec/controllers/spree/products_controller_spec.rb b/spec/controllers/spree/products_controller_spec.rb index dba92be..453e11f 100644 --- a/spec/controllers/spree/products_controller_spec.rb +++ b/spec/controllers/spree/products_controller_spec.rb @@ -11,11 +11,31 @@ controller.stub :spree_current_user => user end - it 'sets @products instance variable' do + it 'sets the right instance variable' do spree_get :google_merchant, format: :rss - assigns(:products).should_not be_nil - assigns(:products).first.should eql(product) + assigns(:items).should_not be_nil + assigns(:items).first.should eql(product) + end + + context 'with full_variants set' do + before do + Spree::GoogleMerchant::Manager.include_variants = true + end + + it 'sets the right instance variable with only master variants' do + spree_get :google_merchant, format: :rss + + assigns(:items).first.should eql(product.master) + end + + it 'sets the right instance variable with multiple variants' do + variant = create(:variant, product: product) + + spree_get :google_merchant, format: :rss + + assigns(:items).first.should eql(variant) + end end it 'renders the proper RSS template' do diff --git a/spec/features/customization_spec.rb b/spec/features/customization_spec.rb new file mode 100644 index 0000000..f03d9f9 --- /dev/null +++ b/spec/features/customization_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + +feature :customization do + stub_authorization! + + background do + reset_manager + sign_in_as! create(:admin_user) + Spree::GoogleMerchant::ProductManager::DEFAULT_MAPPING.each do |key, value| + create(:property, name: key.to_s, presentation: value.tr('_', ' ').titleize) + end + create(:property, name: 'my_brand', presentation: 'My Brand') + create(:option_type, name: 'variant_brand', presentation: 'Variant Brand') + create(:product, available_on: 1.year.ago).tap do |product| + Spree::Property.all.each do |property| + create(:product_property, product: product, property: property, value: property.presentation) + end + create(:variant, product: product).tap do |variant| + Spree::OptionType.all.each do |type| + variant.option_values.create( + option_type: type, name: "v_#{type.name}", presentation: "V: #{type.presentation}" + ) + end + end + end + end + + after do + reset_manager + end + + context 'with product_mapping' do + it 'can remove a key' do + expect { + Spree::GoogleMerchant::Manager.product_mapping.delete(:google_merchant_brand) + }.to change { + visit '/google_merchant.rss'; page.body + }.from( + /Google Product Category<\/g:google_product_category>\nBrand<\/g:brand>\nDepartment<\/g:department>\nColor<\/g:color>\nGtin<\/g:gtin>/ + ).to( + /Google Product Category<\/g:google_product_category>\nDepartment<\/g:department>\nColor<\/g:color>\nGtin<\/g:gtin>/ + ) + end + + it 'can replace a key' do + expect { + Spree::GoogleMerchant::Manager.product_mapping.delete(:google_merchant_brand) + Spree::GoogleMerchant::Manager.product_mapping[:my_brand] = "replaced_brand" + }.to change { + visit '/google_merchant.rss'; page.body + }.from( + /Google Product Category<\/g:google_product_category>\nBrand<\/g:brand>\nDepartment<\/g:department>\nColor<\/g:color>\nGtin<\/g:gtin>/ + ).to( + /Google Product Category<\/g:google_product_category>\nDepartment<\/g:department>\nColor<\/g:color>\nGtin<\/g:gtin>\nMy Brand<\/g:replaced_brand>/ + ) + end + + it 'can default a key for all products' do + expect { + Spree::GoogleMerchant::Manager.product_mapping.delete(:google_merchant_brand) + Spree::GoogleMerchant::Manager.product_fallbacks[:brand] = "Not In the Database" + }.to change { + visit '/google_merchant.rss'; page.body + }.from( + /Google Product Category<\/g:google_product_category>\nBrand<\/g:brand>\nDepartment<\/g:department>\nColor<\/g:color>\nGtin<\/g:gtin>/ + ).to( + /Not In the Database<\/g:brand>\nGoogle Product Category<\/g:google_product_category>\nDepartment<\/g:department>\nColor<\/g:color>\nGtin<\/g:gtin>/ + ) + end + + context "showing variants" do + before do + Spree::GoogleMerchant::Manager.include_variants = true + end + + it "can be overriden by variant values" do + expect { + Spree::GoogleMerchant::Manager.variant_mapping[:variant_brand] = "brand" + }.to change { + visit '/google_merchant.rss'; page.body + }.from( + /Google Product Category<\/g:google_product_category>\nBrand<\/g:brand>/ + ).to( + /Google Product Category<\/g:google_product_category>\nV: Variant Brand<\/g:brand>/ + ) + end + + it "can add keys from the variant" do + expect { + Spree::GoogleMerchant::Manager.variant_mapping[:variant_brand] = "alt_brand" + }.to change { + visit '/google_merchant.rss'; page.body + }.from( + /Google Product Category<\/g:google_product_category>\nBrand<\/g:brand>\nDepartment<\/g:department>\nColor<\/g:color>\nGtin<\/g:gtin>/ + ).to( + /Google Product Category<\/g:google_product_category>\nBrand<\/g:brand>\nDepartment<\/g:department>\nColor<\/g:color>\nGtin<\/g:gtin>\nV: Variant Brand<\/g:alt_brand>/ + ) + end + end + end + + def reset_manager + Spree::GoogleMerchant::Manager.product_mapping = Spree::GoogleMerchant::ProductManager::DEFAULT_MAPPING + Spree::GoogleMerchant::Manager.product_fallbacks = {} + Spree::GoogleMerchant::Manager.variant_mapping = {} + Spree::GoogleMerchant::Manager.include_variants = false + Spree::GoogleMerchant::Manager.advertising_image_proc = ->{order(:position)} + Spree::GoogleMerchant::Manager.weight_unit = Spree::GoogleMerchant::ProductManager::VALID_WEIGHT_UNITS.first + end +end diff --git a/spec/features/products_spec.rb b/spec/features/products_spec.rb index cd9a3ec..b7a51c8 100644 --- a/spec/features/products_spec.rb +++ b/spec/features/products_spec.rb @@ -6,11 +6,11 @@ background do sign_in_as! create(:admin_user) create(:product, available_on: 1.year.ago) - visit '/google_merchant.rss' end - context :show, js: true do + context 'show rss', js: true do it 'shows the RSS feed' do + visit '/google_merchant.rss' xml = Nokogiri::XML(page.body) # ensure 1 product @@ -25,4 +25,16 @@ xml.css("rss").first.css('link').first.children.first.text.should eql(Spree::GoogleMerchant::Config.production_domain) end end + + context 'show xml', js: true do + it 'shows the same content for the XML feed' do + visit '/google_merchant.rss' + rss = page.body + + visit '/google_merchant.xml' + xml = page.body + + expect(xml).to eq(rss) + end + end end diff --git a/spree_google_merchant.gemspec b/spree_google_merchant.gemspec index 239ed64..6eef32f 100644 --- a/spree_google_merchant.gemspec +++ b/spree_google_merchant.gemspec @@ -1,9 +1,9 @@ Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.name = 'spree_google_merchant' - s.version = '1.1.0' - s.summary = 'Google Merchant RSS feed for Spree 2.2' - s.description = 'Google Merchant RSS feed for Spree 2.2' + s.version = '2.4.0' + s.summary = 'Google Merchant RSS feed for Spree 2.4' + s.description = 'Google Merchant RSS feed for Spree 2.4' s.required_ruby_version = '>= 2.0.0' s.author = 'Tim Neems, sebastyuiop, Ben Radler' @@ -16,7 +16,7 @@ Gem::Specification.new do |s| s.require_path = 'lib' s.requirements << 'none' - s.add_dependency 'spree_core', '~> 2.2' + s.add_dependency 'spree_core', '~> 2.4' s.add_dependency 'spree_api' s.add_dependency 'spree_backend' @@ -24,13 +24,11 @@ Gem::Specification.new do |s| s.add_development_dependency 'rspec-rails', '~> 2.13' s.add_development_dependency 'capybara', '2.1.0' s.add_development_dependency 'launchy' - s.add_development_dependency 'sass-rails', '~> 4.0.0' + s.add_development_dependency 'sass-rails', '~> 4.0.2' s.add_development_dependency 'coffee-rails', '~> 4.0.0' s.add_development_dependency 'email_spec', '~> 1.5.0' s.add_development_dependency 'ffaker' s.add_development_dependency 'shoulda-matchers', '~> 1.5' s.add_development_dependency 'database_cleaner', '~> 1.2.0' s.add_development_dependency 'poltergeist', '~> 1.5.0' - # s.add_development_dependency 'selenium-webdriver' - # s.add_development_dependency 'simplecov', '~> 0.7.1' end