kithe 1.1.2 → 2.0.0.pre.alpha1

Sign up to get free protection for your applications and to get access to all the features.
@@ -27,8 +27,12 @@ describe "Kithe::Asset derivative definitions", queue_adapter: :test do
27
27
  ).tap do |a|
28
28
  # We want to promote without create_derivatives being automatically called
29
29
  # as usual, so we can test create_derivatives manually.
30
- a.file_attacher.set_promotion_directives(skip_callbacks: true)
30
+ a.file_attacher.set_promotion_directives(create_derivatives: false)
31
31
  a.promote
32
+
33
+ # Precondition assumptions for our test setup to be valid
34
+ expect(a.file_attacher.stored?).to be(true)
35
+ expect(a.derivatives).to be_empty
32
36
  end
33
37
  end
34
38
 
@@ -101,7 +105,7 @@ describe "Kithe::Asset derivative definitions", queue_adapter: :test do
101
105
  asset.create_derivatives
102
106
 
103
107
  jpg_deriv = asset.derivatives.find {|d| d.key == "a_jpg"}
104
- expect(jpg_deriv.file.storage_key).to eq("kithe_derivatives")
108
+ expect(jpg_deriv.file.storage_key).to eq(:kithe_derivatives)
105
109
  end
106
110
 
107
111
 
@@ -147,7 +151,7 @@ describe "Kithe::Asset derivative definitions", queue_adapter: :test do
147
151
  deriv = asset.derivatives.first
148
152
 
149
153
  expect(deriv).to be_present
150
- expect(deriv.file.storage_key).to eq("store")
154
+ expect(deriv.file.storage_key).to eq(:store)
151
155
  end
152
156
  end
153
157
 
@@ -15,6 +15,28 @@ describe "Kithe::Asset promotion hooks", queue_adapter: :inline do
15
15
  )
16
16
  }
17
17
 
18
+ describe "before_promotion" do
19
+ temporary_class("TestAsset") do
20
+ Class.new(Kithe::Asset) do
21
+ before_promotion do
22
+ $metadata_in_before_promotion = self.file.metadata
23
+ end
24
+ end
25
+ end
26
+ before do
27
+ $metadata_in_before_promotion = nil
28
+ end
29
+
30
+ # we have a built-in before_promotion for metadata extraction,
31
+ # make sure it happens before any additional before_promotions,
32
+ # so they can eg use it to cancel
33
+ it "has access to automatic metadata extraction" do
34
+ unsaved_asset.save!
35
+ expect($metadata_in_before_promotion).to be_present
36
+ expect($metadata_in_before_promotion['sha512']).to be_present
37
+ end
38
+ end
39
+
18
40
  describe "before_promotion cancellation" do
19
41
  temporary_class("TestAsset") do
20
42
  Class.new(Kithe::Asset) do
@@ -28,23 +50,138 @@ describe "Kithe::Asset promotion hooks", queue_adapter: :inline do
28
50
  end
29
51
  end
30
52
 
31
- it "works" do
32
- expect_any_instance_of(Kithe::AssetUploader::Attacher).not_to receive(:store!)
53
+ describe "with inline promotion", queue_adapter: :test do
54
+ before do
55
+ unsaved_asset.file_attacher.set_promotion_directives(promote: :inline)
56
+ end
33
57
 
34
- unsaved_asset.save!
35
- unsaved_asset.reload
36
- expect(unsaved_asset.stored?).to be(false)
58
+ it "cancels" do
59
+ expect_any_instance_of(Kithe::AssetUploader::Attacher).not_to receive(:promote)
60
+
61
+ unsaved_asset.save!
62
+ unsaved_asset.reload
63
+ expect(unsaved_asset.reload.stored?).to be(false)
64
+ end
65
+
66
+ describe "with promotion_directives[:skip_callbacks]" do
67
+ it "doesn't cancel" do
68
+ expect_any_instance_of(Kithe::AssetUploader::Attacher).to receive(:promote).and_call_original
69
+
70
+ unsaved_asset.file_attacher.set_promotion_directives(skip_callbacks: true)
71
+ unsaved_asset.save!
72
+ unsaved_asset.reload
73
+
74
+ expect(unsaved_asset.stored?).to be(true)
75
+ end
76
+ end
37
77
  end
38
78
 
39
- describe "with promotion_directives[:skip_callbacks]" do
40
- it "doesn't cancel" do
41
- expect_any_instance_of(Kithe::AssetUploader::Attacher).to receive(:store!).and_call_original
79
+ describe "with backgrounding promotion", queue_adapter: :inline do
80
+ it "cancels" do
81
+ expect_any_instance_of(Kithe::AssetUploader::Attacher).not_to receive(:promote)
42
82
 
43
- unsaved_asset.file_attacher.set_promotion_directives(skip_callbacks: true)
44
83
  unsaved_asset.save!
45
84
  unsaved_asset.reload
85
+ expect(unsaved_asset.stored?).to be(false)
86
+ end
87
+
88
+ describe "with promotion_directives[:skip_callbacks]" do
89
+ it "doesn't cancel" do
90
+ expect_any_instance_of(Kithe::AssetUploader::Attacher).to receive(:promote).and_call_original
91
+
92
+ unsaved_asset.file_attacher.set_promotion_directives(skip_callbacks: true)
93
+ unsaved_asset.save!
94
+ unsaved_asset.reload
95
+
96
+ expect(unsaved_asset.stored?).to be(true)
97
+ end
98
+ end
99
+ end
100
+
101
+
102
+ describe "assigning directly to store" do
103
+ temporary_class("TestAsset") do
104
+ Class.new(Kithe::Asset) do
105
+ before_promotion do
106
+ raise "Should not call before_promotion"
107
+ end
108
+
109
+ after_promotion do
110
+ raise "Should not call after_promotion"
111
+ end
112
+ end
113
+ end
114
+
115
+ let(:asset) {
116
+ TestAsset.create(title: "test")
117
+ }
118
+
119
+ let(:filepath) { Kithe::Engine.root.join("spec/test_support/images/1x1_pixel.jpg") }
120
+
121
+ describe "with inline promoting" do
122
+ before do
123
+ asset.file_attacher.set_promotion_directives(promote: :inline)
124
+ end
125
+
126
+ it "should not call callbacks" do
127
+ expect_any_instance_of(Kithe::AssetUploader::Attacher).not_to receive(:promote)
128
+
129
+ asset.file_attacher.attach(File.open(filepath))
130
+ asset.save!
131
+
132
+ expect(asset.changed?).to be(false)
133
+ asset.reload
134
+ expect(asset.file).to be_present
135
+ expect(asset.stored?).to be(true)
136
+ end
137
+ end
138
+
139
+ describe "with background promoting", queue_adapter: :inline do
140
+ before do
141
+ asset.file_attacher.set_promotion_directives(promote: :background)
142
+ end
143
+
144
+ it "should not call callbacks" do
145
+ expect_any_instance_of(Kithe::AssetUploader::Attacher).not_to receive(:promote)
146
+
147
+ asset.file_attacher.attach(File.open(filepath))
148
+ asset.save!
149
+
150
+ expect(asset.changed?).to be(false)
151
+ asset.reload
152
+ expect(asset.file).to be_present
153
+ expect(asset.stored?).to be(true)
154
+ end
155
+ end
156
+ end
157
+
158
+
159
+ describe "calling Asset#promote directly", queue_adapter: :inline do
160
+ before do
161
+ unsaved_asset.file_attacher.set_promotion_directives(promote: false)
162
+ unsaved_asset.save!
163
+ # precondition
164
+ expect(unsaved_asset.reload.file_attacher.cached?).to be(true)
165
+ end
166
+
167
+ it "cancels" do
168
+ expect_any_instance_of(Kithe::AssetUploader::Attacher).not_to receive(:promote)
169
+
170
+ unsaved_asset.promote
171
+ unsaved_asset.reload
172
+ expect(unsaved_asset.stored?).to be(false)
173
+ end
174
+
175
+ describe "with promotion_directives[:skip_callbacks]" do
176
+ it "doesn't cancel" do
177
+ expect_any_instance_of(Kithe::AssetUploader::Attacher).to receive(:promote).and_call_original
46
178
 
47
- expect(unsaved_asset.stored?).to be(true)
179
+ unsaved_asset.file_attacher.set_promotion_directives(skip_callbacks: true)
180
+ unsaved_asset.promote
181
+ unsaved_asset.reload
182
+
183
+ expect(unsaved_asset.stored?).to be(true)
184
+ end
48
185
  end
49
186
  end
50
187
  end
@@ -56,7 +193,7 @@ describe "Kithe::Asset promotion hooks", queue_adapter: :inline do
56
193
  receiver = after_promotion_receiver
57
194
  Class.new(Kithe::Asset) do
58
195
  after_promotion do
59
- receiver.call
196
+ receiver.call(self)
60
197
  end
61
198
  end
62
199
  end
@@ -66,6 +203,29 @@ describe "Kithe::Asset promotion hooks", queue_adapter: :inline do
66
203
  unsaved_asset.save!
67
204
  end
68
205
 
206
+ describe "with inline promotion" do
207
+ before do
208
+ unsaved_asset.file_attacher.set_promotion_directives(promote: :inline)
209
+ end
210
+
211
+ # this is actually what's checking for following example...
212
+ let(:after_promotion_receiver) do
213
+ proc do |asset|
214
+ expect(asset.changed?).to be(false)
215
+
216
+ asset.reload
217
+
218
+ expect(asset.stored?).to be(true)
219
+ expect(asset.sha512).to be_present
220
+ end
221
+ end
222
+
223
+ it "asset has metadata and is finalized" do
224
+ expect(after_promotion_receiver).to receive(:call).and_call_original
225
+ unsaved_asset.save!
226
+ end
227
+ end
228
+
69
229
  describe "with promotion_directives[:skip_callbacks]" do
70
230
  it "doesn't call" do
71
231
  expect(after_promotion_receiver).not_to receive(:call)
@@ -178,7 +338,7 @@ describe "Kithe::Asset promotion hooks", queue_adapter: :inline do
178
338
  let!(:existing_file) { saved_asset.file }
179
339
 
180
340
  it "can cancel deletion" do
181
- expect(Kithe::AssetUploader::Attacher).not_to receive(:delete)
341
+ expect_any_instance_of(Kithe::AssetUploader::Attacher).not_to receive(:destroy)
182
342
 
183
343
 
184
344
  saved_asset.set_promotion_directives(delete: false)
@@ -224,5 +384,12 @@ describe "Kithe::Asset promotion hooks", queue_adapter: :inline do
224
384
  asset.set_promotion_directives(promote: :inline)
225
385
  expect(asset.file_attacher.promotion_directives).to eq("promote" => "inline")
226
386
  end
387
+
388
+ it "setting from instance writer is aggregative" do
389
+ Kithe::Asset.promotion_directives = { promote: :inline }
390
+ asset = Kithe::Asset.new
391
+ asset.set_promotion_directives(create_derivatives: false)
392
+ expect(asset.file_attacher.promotion_directives).to eq("promote" => "inline", "create_derivatives" => "false")
393
+ end
227
394
  end
228
395
  end
@@ -107,7 +107,7 @@ RSpec.describe Kithe::Asset, type: :model do
107
107
  asset.save!
108
108
  asset.reload
109
109
 
110
- expect(asset.file.storage_key).to eq(asset.file_attacher.store.storage_key.to_s)
110
+ expect(asset.file.storage_key).to eq(asset.file_attacher.store.storage_key.to_sym)
111
111
  expect(asset.stored?).to be true
112
112
  expect(asset.file.read).to include("Example Response")
113
113
  expect(asset.file.id).to end_with(".html") # no query params
@@ -120,6 +120,7 @@ RSpec.describe Kithe::Asset, type: :model do
120
120
  asset.file = {"id" => "http://www.example.com/bar.html?foo=bar",
121
121
  "storage" => "remote_url",
122
122
  "headers" => {"Authorization" => "Bearer TestToken"}}
123
+
123
124
  asset.save!
124
125
 
125
126
  expect(
@@ -172,7 +173,7 @@ RSpec.describe Kithe::Asset, type: :model do
172
173
  file: File.open(Kithe::Engine.root.join("spec/test_support/images/1x1_pixel.jpg"))
173
174
  ).tap do |a|
174
175
  a.file_attacher.set_promotion_directives(skip_callbacks: true)
175
- a.promote
176
+ #a.promote
176
177
  a.reload
177
178
  a.update_derivative(:existing, StringIO.new("content"))
178
179
  end
@@ -52,7 +52,7 @@ RSpec.describe Kithe::Derivative, type: :model, queue_adapter: :test do
52
52
  # file is stored
53
53
  expect(derivative.key).to eq(key)
54
54
  expect(derivative.file).to be_present
55
- expect(derivative.file.storage_key).to eq("kithe_derivatives")
55
+ expect(derivative.file.storage_key).to eq(:kithe_derivatives)
56
56
  expect(derivative.file.read).to eq(dummy_content)
57
57
 
58
58
  # some metadata we got
@@ -63,7 +63,7 @@ RSpec.describe Kithe::Derivative, type: :model, queue_adapter: :test do
63
63
  expect(derivative.file.metadata["filename"]).to eq("#{asset.friendlier_id}_some_derivative.jpeg")
64
64
 
65
65
  # path on storage is nice and pretty
66
- expect(derivative.file.id).to match %r{\A#{asset.id}/#{key}/[a-f0-9]+\.jpg\Z}
66
+ expect(derivative.file.id).to match %r{\A#{asset.id}/#{key}/[a-f0-9]+\.jpeg\Z}
67
67
  end
68
68
 
69
69
  it "can add a derivative with custom storage location" do
@@ -72,11 +72,11 @@ RSpec.describe Kithe::Derivative, type: :model, queue_adapter: :test do
72
72
  expect(derivative).to be_present
73
73
  derivative.reload
74
74
  expect(derivative.file).to be_present
75
- expect(derivative.file.storage_key).to eq("store")
75
+ expect(derivative.file.storage_key).to eq(:store)
76
76
  end
77
77
 
78
78
  it "can add a derivative with custom metadata" do
79
- derivative = asset.update_derivative(key, dummy_io, metadata: { foo: "bar" })
79
+ derivative = asset.update_derivative(key, dummy_io, metadata: { "foo" => "bar" })
80
80
  expect(derivative).to be_present
81
81
  expect(derivative.file.metadata["size"]).to eq(dummy_content.length)
82
82
  expect(derivative.file.metadata["foo"]).to eq("bar")
@@ -22,7 +22,7 @@ describe Shrine::Plugins::KitheMultiCache do
22
22
  it "can promote from additional cache" do
23
23
  extra_storage.upload(fakeio("test_content"), "test_id")
24
24
  attacher.assign({"id" => "test_id", "storage" => "additional_one"}.to_json)
25
- attacher.promote(attacher.get)
25
+ attacher.promote
26
26
 
27
27
  uploaded_file = attacher.get
28
28
  expect(uploaded_file).not_to be_nil
data/spec/spec_helper.rb CHANGED
@@ -130,7 +130,7 @@ end
130
130
  # https://github.com/shrinerb/shrine/pull/443
131
131
  #
132
132
  require 'sane_patch'
133
- SanePatch.patch("shrine", "< 3") do
133
+ SanePatch.patch("shrine", "< 3.2.2") do
134
134
  require 'shrine/storage/memory'
135
135
  class Shrine::Storage::Memory
136
136
  def open(id, *)
@@ -3,6 +3,7 @@ module ShrineSpecSupport
3
3
  # https://github.com/shrinerb/shrine/blob/203c2c9a0c83815c9ded1b09d5d006b2a523579c/test/support/generic_helper.rb#L6
4
4
  def test_uploader(storage_key = :store, &block)
5
5
  uploader_class = Class.new(Shrine)
6
+ uploader_class.plugin :model
6
7
  uploader_class.storages[:cache] = Shrine::Storage::Test.new
7
8
  uploader_class.storages[:store] = Shrine::Storage::Test.new
8
9
  uploader_class.class_eval(&block) if block
@@ -13,7 +14,7 @@ module ShrineSpecSupport
13
14
  uploader = test_uploader(*args, &block)
14
15
  Object.send(:remove_const, "TestUser") if defined?(TestUser) # for warnings
15
16
  user_class = Object.const_set("TestUser", Struct.new(:avatar_data, :id))
16
- user_class.include uploader.class::Attachment.new(:avatar, **attachment_options)
17
+ user_class.include uploader.class::Attachment(:avatar, **attachment_options)
17
18
  user_class.new.avatar_attacher
18
19
  end
19
20
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kithe
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 2.0.0.pre.alpha1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Rochkind
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-28 00:00:00.000000000 Z
11
+ date: 2020-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -70,14 +70,14 @@ dependencies:
70
70
  requirements:
71
71
  - - "~>"
72
72
  - !ruby/object:Gem::Version
73
- version: '2.14'
73
+ version: '3.2'
74
74
  type: :runtime
75
75
  prerelease: false
76
76
  version_requirements: !ruby/object:Gem::Requirement
77
77
  requirements:
78
78
  - - "~>"
79
79
  - !ruby/object:Gem::Version
80
- version: '2.14'
80
+ version: '3.2'
81
81
  - !ruby/object:Gem::Dependency
82
82
  name: shrine-url
83
83
  requirement: !ruby/object:Gem::Requirement
@@ -376,7 +376,8 @@ files:
376
376
  - lib/kithe/version.rb
377
377
  - lib/shrine/plugins/kithe_accept_remote_url.rb
378
378
  - lib/shrine/plugins/kithe_multi_cache.rb
379
- - lib/shrine/plugins/kithe_promotion_hooks.rb
379
+ - lib/shrine/plugins/kithe_promotion_callbacks.rb
380
+ - lib/shrine/plugins/kithe_promotion_directives.rb
380
381
  - lib/shrine/plugins/kithe_storage_location.rb
381
382
  - lib/tasks/kithe_tasks.rake
382
383
  - lib/vendor/icc/sRGB2014.icc
@@ -483,12 +484,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
483
484
  requirements:
484
485
  - - ">="
485
486
  - !ruby/object:Gem::Version
486
- version: '0'
487
+ version: '2.5'
487
488
  required_rubygems_version: !ruby/object:Gem::Requirement
488
489
  requirements:
489
- - - ">="
490
+ - - ">"
490
491
  - !ruby/object:Gem::Version
491
- version: '0'
492
+ version: 1.3.1
492
493
  requirements: []
493
494
  rubygems_version: 3.0.3
494
495
  signing_key:
@@ -1,138 +0,0 @@
1
- class Shrine
2
- module Plugins
3
- # This adds some features around shrine promotion that we found useful for dealing
4
- # with backgrounding promotion.
5
- #
6
- # * It will run shrine uploader metadata extraction routines on _any promotion_,
7
- # also adding a `promoting: true` key to the shrine context for that metadata
8
- # extraction. (Using shrine refresh_metadata plugin)
9
- #
10
- # * We separately give our Kithe::Asset model class an activemodel callback
11
- # "promotion" hook. This plugin will call those callbacks around promotion (whether background
12
- # or foreground) -- before, after, or around.
13
- #
14
- # If a callback does a `throw :abort` before promotion, it can cancel promotion. This could
15
- # be used to cancel promotion for a validation failure of some kind -- you'd want to somehow
16
- # store or notify what happened, otherwise to the app and it's users it will just look like
17
- # the thing was never promoted for unknown reasons.
18
- #
19
- # After promotion hooks can be used to hook into things you want to do only after a promotion;
20
- # since promotion is backgrounded it would be otherwise inconvenient to execute something
21
- # only after promotion completes.
22
- #
23
- # The default Kithe::Asset hooks into after_promotion to run derivatives creation
24
- # routines.
25
- #
26
- # * A special :promotion_directives key in the shrine context, which will be serialized
27
- # and restored to be preserved even accross background promotion. It is intended to hold
28
- # a hash of arbitrary key/values. The special key :skip_callbacks, when set to a truthy
29
- # value, will prevent the promotion callbacks discussed above from happening. So if you want
30
- # to save a Kithe::Asset and have promotion happen as usual, but _not_ trigger any callbacks
31
- # (including derivative creation):
32
- #
33
- # some_asset.file = some_assignable_file
34
- # some_asset.file_attacher.set_promotion_directives(skip_callbacks: true)
35
- # some_asset.save!
36
- #
37
- # You can add other arbitrary keys which your own code in an uploader or promotion
38
- # callback may consult, with `set_promotion_directives` as above. To consult, check
39
- # attacher.promotion_directives[:some_key]
40
- #
41
- # You can also set promotion directives globally for Kithe::Asset or a sub-class, in
42
- # a class method. Especially useful for batch processing.
43
- #
44
- # Kithe::Asset.promotion_directives = { promote: :inline, create_derivatives: :inline }
45
- #
46
- class KithePromotionHooks
47
- # whitelist of allowed promotion_directive keys, so we can raise on typos but still
48
- # be extensible. Also serves as some documentation of what directives available.
49
- class_attribute :allowed_promotion_directives,
50
- instance_writer: false,
51
- default: [:promote, :skip_callbacks, :create_derivatives, :delete]
52
-
53
- def self.load_dependencies(uploader, *)
54
- uploader.plugin :refresh_metadata
55
- uploader.plugin :backgrounding
56
- end
57
-
58
- module AttacherClassMethods
59
- # Overridden to restore any serialized promotion_directives to context[:promotion_directives],
60
- # in backgrounding promotion.
61
- def load(data)
62
- super.tap do |attacher|
63
- if data["promotion_directives"]
64
- attacher.context[:promotion_directives] = data["promotion_directives"]
65
- end
66
- end
67
- end
68
- end
69
-
70
- module AttacherMethods
71
-
72
- # Set one or more promotion directives, in context[:promotion_directives], that
73
- # will be serialized and restored to context for bg promotion. The values are intended
74
- # to be simple strings or other json-serializable primitives.
75
- #
76
- # set_promotion_directives will merge it's results into existing promotion directives,
77
- # existing keys will remain. So you can set multiple directives with multiple
78
- # calls to set_promotion_directives, or pass multiple keys to one calls.
79
- #
80
- # @example
81
- # some_model.file_attacher.set_promotion_directives(skip_callbacks: true)
82
- # some_model.save!
83
- def set_promotion_directives(hash)
84
- # ActiveJob sometimes has trouble if there are symbols in there, somewhat
85
- # unpredictably.
86
- hash = hash.collect { |k, v| [k.to_s, v === Symbol ? v.to_s : v.to_s]}.to_h
87
-
88
- unrecognized = hash.keys.collect(&:to_sym) - KithePromotionHooks.allowed_promotion_directives
89
- unless unrecognized.length == 0
90
- raise ArgumentError.new("Unrecognized promotion directive key: #{unrecognized.join('')}")
91
- end
92
-
93
- promotion_directives.merge!(hash)
94
- end
95
-
96
- # context[:promotion_directives], lazily initializing to hash for convenience.
97
- def promotion_directives
98
- context[:promotion_directives] ||= {}
99
- end
100
-
101
- # Overridden so our context[:promotion_directives] is serialized for
102
- # backgrounding.
103
- def dump
104
- super.tap do |hash|
105
- if context[:promotion_directives]
106
- hash["promotion_directives"] = context[:promotion_directives]
107
- end
108
- end
109
- end
110
-
111
- # Overridden to:
112
- # a) refresh metadata as part of promotion (adds `promoting: true` to context for such)
113
- # b) call promotion callbacks on Asset model, unless `promotion_directives["skip_callbacks"]`
114
- # has been set.
115
- def promote(uploaded_file = get, **options)
116
- # insist on a metadata extraction, add a new key `promoting: true` in case
117
- # anyone is interested.
118
-
119
- uploaded_file.refresh_metadata!(**context.merge(options).merge(promoting: true))
120
-
121
- # Now run ordinary promotion with activemodel callbacks from
122
- # the Asset, which will automatically allow them to cancel promotion using
123
- # ordinary activemodel callbacck technique of `throw :abort`.
124
- if ( !promotion_directives["skip_callbacks"] &&
125
- context[:record] &&
126
- context[:record].class.respond_to?(:_promotion_callbacks) )
127
- context[:record].run_callbacks(:promotion) do
128
- super(uploaded_file, **options)
129
- end
130
- else
131
- super(uploaded_file, **options)
132
- end
133
- end
134
- end
135
- end
136
- register_plugin(:kithe_promotion_hooks, KithePromotionHooks)
137
- end
138
- end