Skip to content

Commit 5c1e69b

Browse files
authored
Migrate add_attributes_to_categories command to new tool (#586)
2 parents bc9c03e + ac74079 commit 5c1e69b

File tree

6 files changed

+298
-0
lines changed

6 files changed

+298
-0
lines changed

dev/lib/product_taxonomy.rb

+1
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ def data_path = DATA_PATH
5454
require_relative "product_taxonomy/commands/sync_en_localizations_command"
5555
require_relative "product_taxonomy/commands/add_category_command"
5656
require_relative "product_taxonomy/commands/add_attribute_command"
57+
require_relative "product_taxonomy/commands/add_attributes_to_categories_command"
5758
require_relative "product_taxonomy/commands/add_value_command"

dev/lib/product_taxonomy/cli.rb

+7
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ def add_attribute(name, description)
7575
AddAttributeCommand.new(options.merge(name:, description:)).run
7676
end
7777

78+
desc "add_attributes_to_categories ATTRIBUTE_FRIENDLY_IDS CATEGORY_IDS",
79+
"Add one or more attributes to one or more categories. ATTRIBUTE_FRIENDLY_IDS is a comma-separated list of attribute friendly IDs."
80+
option :include_descendants, type: :boolean, desc: "When set, the attributes will be added to all descendants of the specified categories"
81+
def add_attributes_to_categories(attribute_friendly_ids, category_ids)
82+
AddAttributesToCategoriesCommand.new(options.merge(attribute_friendly_ids:, category_ids:)).run
83+
end
84+
7885
desc "add_value NAME ATTRIBUTE_FRIENDLY_ID", "Add a new value with NAME to the primary attribute with ATTRIBUTE_FRIENDLY_ID"
7986
def add_value(name, attribute_friendly_id)
8087
AddValueCommand.new(options.merge(name:, attribute_friendly_id:)).run
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# frozen_string_literal: true
2+
3+
module ProductTaxonomy
4+
class AddAttributesToCategoriesCommand < Command
5+
def initialize(options)
6+
super
7+
load_taxonomy
8+
@attribute_friendly_ids = options[:attribute_friendly_ids]
9+
@category_ids = options[:category_ids]
10+
@include_descendants = options[:include_descendants]
11+
end
12+
13+
def execute
14+
add_attributes_to_categories!
15+
update_data_files!
16+
end
17+
18+
private
19+
20+
def add_attributes_to_categories!
21+
@attributes = attribute_friendly_ids.map do |friendly_id|
22+
attribute = Attribute.find_by(friendly_id:)
23+
next attribute if attribute
24+
25+
raise "Attribute with friendly ID `#{friendly_id}` not found"
26+
end
27+
28+
@categories = category_ids.map do |id|
29+
category = Category.find_by(id:)
30+
next category if category
31+
32+
raise "Category with ID `#{id}` not found"
33+
end
34+
35+
@categories = @categories.flat_map(&:descendants_and_self) if @include_descendants
36+
37+
@categories.each do |category|
38+
@attributes.each do |attribute|
39+
if category.attributes.include?(attribute)
40+
logger.info("Category `#{category.name}` already has attribute `#{attribute.friendly_id}` - skipping")
41+
else
42+
category.add_attribute(attribute)
43+
end
44+
end
45+
end
46+
47+
logger.info("Added #{@attributes.size} attribute(s) to #{@categories.size} categories")
48+
end
49+
50+
def update_data_files!
51+
roots = @categories.map(&:root).uniq.map(&:id)
52+
DumpCategoriesCommand.new(verticals: roots).execute
53+
SyncEnLocalizationsCommand.new(targets: "categories").execute
54+
GenerateDocsCommand.new({}).execute
55+
end
56+
57+
def attribute_friendly_ids
58+
@attribute_friendly_ids.split(",").map(&:strip)
59+
end
60+
61+
def category_ids
62+
@category_ids.split(",").map(&:strip)
63+
end
64+
end
65+
end

dev/lib/product_taxonomy/models/category.rb

+7
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ def add_secondary_child(child)
124124
child.secondary_parents << self
125125
end
126126

127+
# Add an attribute to the category
128+
#
129+
# @param [Attribute] attribute
130+
def add_attribute(attribute)
131+
@attributes << attribute
132+
end
133+
127134
#
128135
# Information
129136
#
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
module ProductTaxonomy
6+
class AddAttributesToCategoriesCommandTest < TestCase
7+
setup do
8+
@color_attribute = Attribute.new(
9+
id: 1,
10+
name: "Color",
11+
description: "Defines the primary color or pattern",
12+
friendly_id: "color",
13+
handle: "color",
14+
values: [
15+
Value.new(
16+
id: 1,
17+
name: "Black",
18+
friendly_id: "color__black",
19+
handle: "color__black",
20+
),
21+
],
22+
)
23+
@size_attribute = Attribute.new(
24+
id: 2,
25+
name: "Size",
26+
description: "Defines the size of the product",
27+
friendly_id: "size",
28+
handle: "size",
29+
values: [
30+
Value.new(
31+
id: 2,
32+
name: "Small",
33+
friendly_id: "size__small",
34+
handle: "size__small",
35+
),
36+
],
37+
)
38+
39+
Attribute.add(@color_attribute)
40+
Attribute.add(@size_attribute)
41+
Value.add(@color_attribute.values.first)
42+
Value.add(@size_attribute.values.first)
43+
44+
@root = Category.new(id: "aa", name: "Apparel & Accessories")
45+
@clothing = Category.new(id: "aa-1", name: "Clothing")
46+
@shirts = Category.new(id: "aa-1-1", name: "Shirts")
47+
@root.add_child(@clothing)
48+
@clothing.add_child(@shirts)
49+
50+
Category.add(@root)
51+
Category.add(@clothing)
52+
Category.add(@shirts)
53+
54+
AddAttributesToCategoriesCommand.any_instance.stubs(:load_taxonomy)
55+
DumpCategoriesCommand.any_instance.stubs(:load_taxonomy)
56+
SyncEnLocalizationsCommand.any_instance.stubs(:load_taxonomy)
57+
GenerateDocsCommand.any_instance.stubs(:load_taxonomy)
58+
end
59+
60+
test "execute adds attributes to specified categories" do
61+
DumpCategoriesCommand.any_instance.expects(:execute).once
62+
SyncEnLocalizationsCommand.any_instance.expects(:execute).once
63+
GenerateDocsCommand.any_instance.expects(:execute).once
64+
65+
AddAttributesToCategoriesCommand.new(
66+
attribute_friendly_ids: "color,size",
67+
category_ids: "aa-1",
68+
include_descendants: false
69+
).execute
70+
71+
assert_equal 2, @clothing.attributes.size
72+
assert_includes @clothing.attributes, @color_attribute
73+
assert_includes @clothing.attributes, @size_attribute
74+
75+
assert_empty @root.attributes
76+
assert_empty @shirts.attributes
77+
end
78+
79+
test "execute adds attributes to categories and their descendants when include_descendants is true" do
80+
DumpCategoriesCommand.any_instance.expects(:execute).once
81+
SyncEnLocalizationsCommand.any_instance.expects(:execute).once
82+
GenerateDocsCommand.any_instance.expects(:execute).once
83+
84+
AddAttributesToCategoriesCommand.new(
85+
attribute_friendly_ids: "color",
86+
category_ids: "aa-1",
87+
include_descendants: true
88+
).execute
89+
90+
assert_equal 1, @clothing.attributes.size
91+
assert_includes @clothing.attributes, @color_attribute
92+
93+
assert_equal 1, @shirts.attributes.size
94+
assert_includes @shirts.attributes, @color_attribute
95+
96+
assert_empty @root.attributes
97+
end
98+
99+
test "execute adds attributes to multiple categories" do
100+
DumpCategoriesCommand.any_instance.expects(:execute).once
101+
SyncEnLocalizationsCommand.any_instance.expects(:execute).once
102+
GenerateDocsCommand.any_instance.expects(:execute).once
103+
104+
AddAttributesToCategoriesCommand.new(
105+
attribute_friendly_ids: "color",
106+
category_ids: "aa,aa-1",
107+
include_descendants: false
108+
).execute
109+
110+
assert_equal 1, @root.attributes.size
111+
assert_includes @root.attributes, @color_attribute
112+
113+
assert_equal 1, @clothing.attributes.size
114+
assert_includes @clothing.attributes, @color_attribute
115+
116+
assert_empty @shirts.attributes
117+
end
118+
119+
test "execute skips adding attributes that are already present" do
120+
@clothing.add_attribute(@color_attribute)
121+
122+
DumpCategoriesCommand.any_instance.expects(:execute).once
123+
SyncEnLocalizationsCommand.any_instance.expects(:execute).once
124+
GenerateDocsCommand.any_instance.expects(:execute).once
125+
126+
AddAttributesToCategoriesCommand.new(
127+
attribute_friendly_ids: "color,size",
128+
category_ids: "aa-1",
129+
include_descendants: false
130+
).execute
131+
132+
assert_equal 2, @clothing.attributes.size
133+
assert_includes @clothing.attributes, @color_attribute
134+
assert_includes @clothing.attributes, @size_attribute
135+
136+
assert_equal 1, @clothing.attributes.count { |attr| attr == @color_attribute }
137+
end
138+
139+
test "execute raises error when attribute is not found" do
140+
assert_raises(RuntimeError) do
141+
AddAttributesToCategoriesCommand.new(
142+
attribute_friendly_ids: "nonexistent",
143+
category_ids: "aa-1",
144+
include_descendants: false
145+
).execute
146+
end
147+
end
148+
149+
test "execute raises error when category is not found" do
150+
assert_raises(RuntimeError) do
151+
AddAttributesToCategoriesCommand.new(
152+
attribute_friendly_ids: "color",
153+
category_ids: "nonexistent",
154+
include_descendants: false
155+
).execute
156+
end
157+
end
158+
159+
test "execute updates data files for all affected root categories" do
160+
# When adding attributes to categories from different verticals,
161+
# the command should update data files for all affected root categories
162+
@second_root = Category.new(id: "bb", name: "Business & Industrial")
163+
@equipment = Category.new(id: "bb-1", name: "Equipment")
164+
165+
@second_root.add_child(@equipment)
166+
167+
Category.add(@second_root)
168+
Category.add(@equipment)
169+
170+
dump_command = mock
171+
dump_command.expects(:execute).once
172+
DumpCategoriesCommand.expects(:new).with(verticals: ["aa", "bb"]).returns(dump_command)
173+
174+
SyncEnLocalizationsCommand.any_instance.expects(:execute).once
175+
GenerateDocsCommand.any_instance.expects(:execute).once
176+
177+
AddAttributesToCategoriesCommand.new(
178+
attribute_friendly_ids: "color",
179+
category_ids: "aa-1,bb-1",
180+
include_descendants: false
181+
).execute
182+
183+
assert_equal 1, @clothing.attributes.size
184+
assert_includes @clothing.attributes, @color_attribute
185+
186+
assert_equal 1, @equipment.attributes.size
187+
assert_includes @equipment.attributes, @color_attribute
188+
end
189+
end
190+
end

dev/test/models/category_test.rb

+28
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,34 @@ class CategoryTest < TestCase
3131
assert_equal [root, root2], child.secondary_parents
3232
end
3333

34+
test "add_attribute adds multiple attributes to category's attributes" do
35+
category = Category.new(id: "aa", name: "Root")
36+
color_attribute = Attribute.new(
37+
id: 1,
38+
name: "Color",
39+
friendly_id: "color",
40+
handle: "color",
41+
description: "Defines the primary color",
42+
values: []
43+
)
44+
45+
size_attribute = Attribute.new(
46+
id: 2,
47+
name: "Size",
48+
friendly_id: "size",
49+
handle: "size",
50+
description: "Defines the size",
51+
values: []
52+
)
53+
54+
category.add_attribute(color_attribute)
55+
category.add_attribute(size_attribute)
56+
57+
assert_includes category.attributes, color_attribute
58+
assert_includes category.attributes, size_attribute
59+
assert_equal 2, category.attributes.size
60+
end
61+
3462
test "root? is true for root node" do
3563
assert @root.root?
3664
end

0 commit comments

Comments
 (0)