diff --git a/app/models/spree/classification.rb b/app/models/spree/classification.rb new file mode 100644 index 0000000000..0b668a068b --- /dev/null +++ b/app/models/spree/classification.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Spree + class Classification < ActiveRecord::Base + self.table_name = 'spree_products_taxons' + belongs_to :product, class_name: "Spree::Product", touch: true + belongs_to :taxon, class_name: "Spree::Taxon", touch: true + + before_destroy :dont_destroy_if_primary_taxon + + private + + def dont_destroy_if_primary_taxon + return unless product.primary_taxon == taxon + + errors.add :base, I18n.t(:spree_classification_primary_taxon_error, taxon: taxon.name, + product: product.name) + false + end + end +end diff --git a/app/models/spree/classification_decorator.rb b/app/models/spree/classification_decorator.rb deleted file mode 100644 index 20608bad94..0000000000 --- a/app/models/spree/classification_decorator.rb +++ /dev/null @@ -1,15 +0,0 @@ -Spree::Classification.class_eval do - belongs_to :product, class_name: "Spree::Product", touch: true - belongs_to :taxon, class_name: "Spree::Taxon", touch: true - - before_destroy :dont_destroy_if_primary_taxon - - private - - def dont_destroy_if_primary_taxon - if product.primary_taxon == taxon - errors.add :base, I18n.t(:spree_classification_primary_taxon_error, taxon: taxon.name, product: product.name) - false - end - end -end diff --git a/app/models/spree/taxon.rb b/app/models/spree/taxon.rb new file mode 100644 index 0000000000..4e29c068ce --- /dev/null +++ b/app/models/spree/taxon.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +module Spree + class Taxon < ActiveRecord::Base + acts_as_nested_set dependent: :destroy + + belongs_to :taxonomy, class_name: 'Spree::Taxonomy', touch: true + has_many :classifications, dependent: :destroy + has_many :products, through: :classifications + + before_create :set_permalink + + validates :name, presence: true + + has_attached_file :icon, + styles: { mini: '32x32>', normal: '128x128>' }, + default_style: :mini, + url: '/spree/taxons/:id/:style/:basename.:extension', + path: ':rails_root/public/spree/taxons/:id/:style/:basename.:extension', + default_url: '/assets/default_taxon.png' + + include Spree::Core::S3Support + supports_s3 :icon + + # Indicate which filters should be used for this taxon + def applicable_filters + [] + end + + # Return meta_title if set otherwise generates from root name and/or taxon name + def seo_title + if meta_title + meta_title + else + root? ? name : "#{root.name} - #{name}" + end + end + + # Creates permalink based on Stringex's .to_url method + def set_permalink + if parent.present? + self.permalink = [parent.permalink, permalink_end].join('/') + elsif permalink.blank? + self.permalink = name.to_url + end + end + + # For #2759 + def to_param + permalink + end + + def active_products + scope = products.active + scope + end + + def pretty_name + ancestor_chain = ancestors.inject("") do |name, ancestor| + name += "#{ancestor.name} -> " + end + ancestor_chain + name.to_s + end + + # Find all the taxons of supplied products for each enterprise, indexed by enterprise. + # Format: {enterprise_id => [taxon_id, ...]} + def self.supplied_taxons + taxons = {} + + Spree::Taxon. + joins(products: :supplier). + select('spree_taxons.*, enterprises.id AS enterprise_id'). + each do |t| + taxons[t.enterprise_id.to_i] ||= Set.new + taxons[t.enterprise_id.to_i] << t.id + end + + taxons + end + + # Find all the taxons of distributed products for each enterprise, indexed by enterprise. + # May return :all taxons (distributed in open and closed order cycles), + # or :current taxons (distributed in an open order cycle). + # + # Format: {enterprise_id => [taxon_id, ...]} + def self.distributed_taxons(which_taxons = :all) + ents_and_vars = ExchangeVariant.joins(exchange: :order_cycle).merge(Exchange.outgoing) + .select("DISTINCT variant_id, receiver_id AS enterprise_id") + + ents_and_vars = ents_and_vars.merge(OrderCycle.active) if which_taxons == :current + + taxons = Spree::Taxon + .select("DISTINCT spree_taxons.id, ents_and_vars.enterprise_id") + .joins(products: :variants_including_master) + .joins(" + INNER JOIN (#{ents_and_vars.to_sql}) AS ents_and_vars + ON spree_variants.id = ents_and_vars.variant_id") + + taxons.each_with_object({}) do |t, ts| + ts[t.enterprise_id.to_i] ||= Set.new + ts[t.enterprise_id.to_i] << t.id + end + end + + private + + def permalink_end + return name.to_url if permalink.blank? + + permalink.split('/').last + end + end +end diff --git a/app/models/spree/taxon_decorator.rb b/app/models/spree/taxon_decorator.rb deleted file mode 100644 index f2c6f1e20a..0000000000 --- a/app/models/spree/taxon_decorator.rb +++ /dev/null @@ -1,47 +0,0 @@ -Spree::Taxon.class_eval do - has_many :classifications, dependent: :destroy - - # Indicate which filters should be used for this taxon - def applicable_filters - fs = [] - # fs << Spree::ProductFilters.distributor_filter if Spree::ProductFilters.respond_to? :distributor_filter - fs - end - - # Find all the taxons of supplied products for each enterprise, indexed by enterprise. - # Format: {enterprise_id => [taxon_id, ...]} - def self.supplied_taxons - taxons = {} - - Spree::Taxon. - joins(products: :supplier). - select('spree_taxons.*, enterprises.id AS enterprise_id'). - each do |t| - taxons[t.enterprise_id.to_i] ||= Set.new - taxons[t.enterprise_id.to_i] << t.id - end - - taxons - end - - # Find all the taxons of distributed products for each enterprise, indexed by enterprise. - # May return :all taxons (distributed in open and closed order cycles), - # or :current taxons (distributed in an open order cycle). - # - # Format: {enterprise_id => [taxon_id, ...]} - def self.distributed_taxons(which_taxons = :all) - ents_and_vars = ExchangeVariant.joins(exchange: :order_cycle).merge(Exchange.outgoing) - .select("DISTINCT variant_id, receiver_id AS enterprise_id") - - ents_and_vars = ents_and_vars.merge(OrderCycle.active) if which_taxons == :current - - taxons = Spree::Taxon - .select("DISTINCT spree_taxons.id, ents_and_vars.enterprise_id").joins(products: :variants_including_master) - .joins("INNER JOIN (#{ents_and_vars.to_sql}) AS ents_and_vars ON spree_variants.id = ents_and_vars.variant_id") - - taxons.each_with_object({}) do |t, ts| - ts[t.enterprise_id.to_i] ||= Set.new - ts[t.enterprise_id.to_i] << t.id - end - end -end diff --git a/app/models/spree/taxonomy.rb b/app/models/spree/taxonomy.rb new file mode 100644 index 0000000000..2934281b4d --- /dev/null +++ b/app/models/spree/taxonomy.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Spree + class Taxonomy < ActiveRecord::Base + validates :name, presence: true + + has_many :taxons + has_one :root, -> { where parent_id: nil }, class_name: "Spree::Taxon", dependent: :destroy + + after_save :set_name + + default_scope -> { order("#{table_name}.position") } + + private + + def set_name + if root + root.update_column(:name, name) + else + self.root = Taxon.create!(taxonomy_id: id, name: name) + end + end + end +end diff --git a/spec/models/spree/taxon_spec.rb b/spec/models/spree/taxon_spec.rb index 54a0069da9..e6d55b4b23 100644 --- a/spec/models/spree/taxon_spec.rb +++ b/spec/models/spree/taxon_spec.rb @@ -1,7 +1,11 @@ +# frozen_string_literal: true + require 'spec_helper' module Spree describe Taxon do + let(:taxon) { Spree::Taxon.new(name: "Ruby on Rails") } + let(:e) { create(:supplier_enterprise) } let!(:t1) { create(:taxon) } let!(:t2) { create(:taxon) } @@ -46,5 +50,51 @@ module Spree end.to change { taxon2.reload.updated_at } end end + + context "set_permalink" do + it "should set permalink correctly when no parent present" do + taxon.set_permalink + expect(taxon.permalink).to eq "ruby-on-rails" + end + + it "should support Chinese characters" do + taxon.name = "你好" + taxon.set_permalink + expect(taxon.permalink).to eq 'ni-hao' + end + + context "with parent taxon" do + before do + allow(taxon).to receive_messages parent_id: 123 + allow(taxon).to receive_messages parent: build(:taxon, permalink: "brands") + end + + it "should set permalink correctly when taxon has parent" do + taxon.set_permalink + expect(taxon.permalink).to eq "brands/ruby-on-rails" + end + + it "should set permalink correctly with existing permalink present" do + taxon.permalink = "b/rubyonrails" + taxon.set_permalink + expect(taxon.permalink).to eq "brands/rubyonrails" + end + + it "should support Chinese characters" do + taxon.name = "我" + taxon.set_permalink + expect(taxon.permalink).to eq "brands/wo" + end + end + end + + # Regression test for Spree #2620 + context "creating a child node using first_or_create" do + let(:taxonomy) { create(:taxonomy) } + + it "does not error out" do + expect { taxonomy.root.children.where(name: "Some name").first_or_create }.not_to raise_error + end + end end end diff --git a/spec/models/spree/taxonomy_spec.rb b/spec/models/spree/taxonomy_spec.rb new file mode 100644 index 0000000000..2ead3dcc0e --- /dev/null +++ b/spec/models/spree/taxonomy_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Spree::Taxonomy do + context "#destroy" do + before do + @taxonomy = create(:taxonomy) + @root_taxon = @taxonomy.root + @child_taxon = create(:taxon, taxonomy_id: @taxonomy.id, parent: @root_taxon) + end + + it "should destroy all associated taxons" do + @taxonomy.destroy + expect{ Spree::Taxon.find(@root_taxon.id) }.to raise_error(ActiveRecord::RecordNotFound) + expect{ Spree::Taxon.find(@child_taxon.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end