contentful-scheduler-custom 1.4.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.
data/example/config.ru ADDED
@@ -0,0 +1,13 @@
1
+ require 'resque'
2
+ require 'resque/server'
3
+ require 'resque/scheduler/server'
4
+
5
+ config = {
6
+ host: '127.0.0.1',
7
+ port: '6379',
8
+ password: '5D40001BB59A2EF2439A55DAB7E718609A3A93CC9D3FD414BB13A75933048CD7'
9
+ }
10
+ Resque.redis = config
11
+
12
+ run Rack::URLMap.new \
13
+ "/" => Resque::Server.new
@@ -0,0 +1,57 @@
1
+ require 'resque'
2
+ require 'redis'
3
+ require 'logger'
4
+ require 'contentful/webhook/listener'
5
+
6
+ require_relative 'scheduler/controller'
7
+ require_relative 'scheduler/version'
8
+
9
+ module Contentful
10
+ module Scheduler
11
+ DEFAULT_PORT = 32123
12
+ DEFAULT_ENDPOINT = '/scheduler'
13
+ DEFAULT_LOGGER = ::Contentful::Webhook::Listener::Support::NullLogger.new
14
+
15
+ @@config = nil
16
+
17
+ def self.config=(config)
18
+ fail ':redis configuration missing' unless config.key?(:redis)
19
+ fail ':spaces configuration missing' unless config.key?(:spaces)
20
+ config[:spaces].each do |space, data|
21
+ fail ":management_token missing for space: #{space}" unless data.key?(:management_token)
22
+ end
23
+
24
+ config[:port] = (ENV.key?('PORT') ? ENV['PORT'].to_i : DEFAULT_PORT) unless config.key?(:port)
25
+ config[:logger] = DEFAULT_LOGGER unless config.key?(:logger)
26
+ config[:endpoint] = DEFAULT_ENDPOINT unless config.key?(:endpoint)
27
+
28
+ ::Resque.redis = config[:redis].dup
29
+ @@config ||= config
30
+ end
31
+
32
+ def self.config
33
+ @@config
34
+ end
35
+
36
+ def self.start(config = {})
37
+ fail "Scheduler not configured" if self.config.nil? && !block_given?
38
+
39
+ if block_given?
40
+ yield(config) if block_given?
41
+ self.config = config
42
+ end
43
+
44
+ ::Contentful::Webhook::Listener::Server.start do |config|
45
+ config[:port] = self.config[:port]
46
+ config[:logger] = self.config[:logger]
47
+ config[:endpoints] = [
48
+ {
49
+ endpoint: self.config[:endpoint],
50
+ controller: ::Contentful::Scheduler::Controller,
51
+ timeout: 0
52
+ }
53
+ ]
54
+ end.join
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,30 @@
1
+ require 'contentful/webhook/listener'
2
+ require_relative 'queue'
3
+
4
+ module Contentful
5
+ module Scheduler
6
+ class Controller < ::Contentful::Webhook::Listener::Controllers::WebhookAware
7
+ def create
8
+ return unless webhook.entry?
9
+
10
+ logger.info "Queueing - Space: #{webhook.space_id} - Entry: #{webhook.id}"
11
+
12
+ Queue.instance(logger).update_or_create(webhook)
13
+ end
14
+ alias_method :save, :create
15
+ alias_method :auto_save, :create
16
+ alias_method :unarchive, :create
17
+
18
+ def delete
19
+ return unless webhook.entry?
20
+
21
+ logger.info "Unqueueing - Space: #{webhook.space_id} - Entry: #{webhook.id}"
22
+
23
+ Queue.instance(logger).remove(webhook)
24
+ end
25
+ alias_method :unpublish, :delete
26
+ alias_method :archive, :delete
27
+ alias_method :publish, :delete
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,147 @@
1
+ require_relative "tasks"
2
+ require 'chronic'
3
+ require 'contentful/webhook/listener'
4
+
5
+ module Contentful
6
+ module Scheduler
7
+ class Queue
8
+ @@instance = nil
9
+
10
+ attr_reader :config, :logger
11
+
12
+ def self.instance(logger = ::Contentful::Webhook::Listener::Support::NullLogger.new)
13
+ @@instance ||= new(logger)
14
+ end
15
+
16
+ def update_or_create(webhook)
17
+ return unless publishable?(webhook)
18
+ remove(webhook) if in_queue?(webhook)
19
+ return if already_published?(webhook)
20
+
21
+ success = Resque.enqueue_at(
22
+ publish_date(webhook),
23
+ ::Contentful::Scheduler::Tasks::Publish,
24
+ webhook.space_id,
25
+ webhook.id,
26
+ ::Contentful::Scheduler.config[:spaces][webhook.space_id][:management_token]
27
+ )
28
+
29
+ updateContentBlocks(webhook)
30
+
31
+ if success
32
+ logger.info "Webhook {id: #{webhook.id}, space_id: #{webhook.space_id}} successfully added to queue"
33
+ else
34
+ logger.warn "Webhook {id: #{webhook.id}, space_id: #{webhook.space_id}} couldn't be added to queue"
35
+ end
36
+ end
37
+
38
+ def remove(webhook)
39
+ return unless publishable?(webhook)
40
+ return unless in_queue?(webhook)
41
+
42
+ success = Resque.remove_delayed(
43
+ ::Contentful::Scheduler::Tasks::Publish,
44
+ webhook.space_id,
45
+ webhook.id,
46
+ ::Contentful::Scheduler.config[:management_token]
47
+ )
48
+
49
+ removeContentBlocks(webhook)
50
+
51
+ if success
52
+ logger.info "Webhook {id: #{webhook.id}, space_id: #{webhook.space_id}} successfully removed from queue"
53
+ else
54
+ logger.warn "Webhook {id: #{webhook.id}, space_id: #{webhook.space_id}} couldn't be removed from queue"
55
+ end
56
+ end
57
+
58
+ def updateContentBlocks(webhook)
59
+ if isContentBlockAvailable(webhook)
60
+ webhook.fields['contentBlocks']['fi-FI'].each do |sys|
61
+ success = Resque.enqueue_at(
62
+ publish_date(webhook),
63
+ ::Contentful::Scheduler::Tasks::Publish,
64
+ webhook.space_id,
65
+ sys['sys']['id'],
66
+ ::Contentful::Scheduler.config[:spaces][webhook.space_id][:management_token]
67
+ )
68
+ if success
69
+ logger.info "Webhook Content block {id: #{sys['sys']['id']}, space_id: #{webhook.space_id}} successfully added to queue"
70
+ else
71
+ logger.warn "Webhook Content block {id: #{sys['sys']['id']}, space_id: #{webhook.space_id}} couldn't be added to queue"
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def removeContentBlocks(webhook)
78
+ if isContentBlockAvailable(webhook)
79
+ webhook.fields['contentBlocks']['fi-FI'].each do |sys|
80
+ success = Resque.remove_delayed(
81
+ ::Contentful::Scheduler::Tasks::Publish,
82
+ webhook.space_id,
83
+ sys['sys']['id'],
84
+ ::Contentful::Scheduler.config[:management_token]
85
+ )
86
+ if success
87
+ logger.info "Webhook Content Block {id: #{sys['sys']['id']}, space_id: #{webhook.space_id}} successfully removed from queue"
88
+ else
89
+ logger.warn "Webhook Content Block {id: #{sys['sys']['id']}, space_id: #{webhook.space_id}} couldn't be removed from queue"
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ def isContentBlockAvailable(webhook)
96
+ return !webhook.fields['contentBlocks'].nil?
97
+ end
98
+
99
+ def publishable?(webhook)
100
+ return false unless spaces.key?(webhook.space_id)
101
+
102
+ if webhook_publish_field?(webhook)
103
+ return !webhook_publish_field(webhook).nil?
104
+ end
105
+
106
+ false
107
+ end
108
+
109
+ def already_published?(webhook)
110
+ return true if publish_date(webhook) < Time.now.utc
111
+
112
+ false
113
+ end
114
+
115
+ def in_queue?(webhook)
116
+ Resque.peek(::Contentful::Scheduler::Tasks::Publish, 0, -1).any? do |job|
117
+ job['args'][0] == webhook.space_id && job['args'][1] == webhook.id
118
+ end
119
+ end
120
+
121
+ def publish_date(webhook)
122
+ date_field = webhook_publish_field(webhook)
123
+ date_field = date_field[date_field.keys[0]] if date_field.is_a? Hash
124
+ Chronic.parse(date_field).utc
125
+ end
126
+
127
+ def spaces
128
+ config[:spaces]
129
+ end
130
+
131
+ def webhook_publish_field?(webhook)
132
+ webhook.fields.key?(spaces.fetch(webhook.space_id, {})[:publish_field])
133
+ end
134
+
135
+ def webhook_publish_field(webhook)
136
+ webhook.fields[spaces[webhook.space_id][:publish_field]]
137
+ end
138
+
139
+ private
140
+
141
+ def initialize(logger)
142
+ @config = ::Contentful::Scheduler.config
143
+ @logger = logger
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1 @@
1
+ require_relative 'tasks/publish'
@@ -0,0 +1,21 @@
1
+ require 'contentful/management'
2
+
3
+ module Contentful
4
+ module Scheduler
5
+ module Tasks
6
+ class Publish
7
+ @queue = :publish
8
+
9
+ def self.perform(space_id, entry_id, token)
10
+ client = ::Contentful::Management::Client.new(
11
+ token,
12
+ raise_errors: true,
13
+ application_name: 'contentful-scheduler',
14
+ application_version: Contentful::Scheduler::VERSION
15
+ )
16
+ client.entries.find(space_id, entry_id).publish
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module Contentful
2
+ module Scheduler
3
+ VERSION = "1.4.0"
4
+ end
5
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Contentful::Scheduler::Controller do
4
+ let(:server) { MockServer.new }
5
+ let(:logger) { Contentful::Webhook::Listener::Support::NullLogger.new }
6
+ let(:timeout) { 10 }
7
+ let(:headers) { {'X-Contentful-Topic' => '', 'X-Contentful-Webhook-Name' => 'SomeName'} }
8
+ let(:body) { {sys: { id: 'foo', space: { sys: { id: 'space_foo' } } }, fields: {} } }
9
+ let(:queue) { ::Contentful::Scheduler::Queue.instance }
10
+ subject { described_class.new server, logger, timeout }
11
+
12
+ describe 'events' do
13
+ [:create, :save, :auto_save, :unarchive].each do |event|
14
+ it "creates or updates webhook metadata in publish queue on #{event}" do
15
+ expect(queue).to receive(:update_or_create)
16
+
17
+ headers['X-Contentful-Topic'] = "ContentfulManagement.Entry.#{event}"
18
+ request = RequestDummy.new(headers, body)
19
+ subject.respond(request, MockResponse.new).join
20
+ end
21
+ end
22
+
23
+ [:delete, :unpublish, :archive, :publish].each do |event|
24
+ it "deletes webhook metadata in publish queue on #{event}" do
25
+ expect(queue).to receive(:remove)
26
+
27
+ headers['X-Contentful-Topic'] = "ContentfulManagement.Entry.#{event}"
28
+ request = RequestDummy.new(headers, body)
29
+ subject.respond(request, MockResponse.new).join
30
+ end
31
+ end
32
+
33
+ [:create, :save, :unarchive, :delete, :unpublish, :archive, :publish].each do |event|
34
+ ['Asset', 'ContentType'].each do |kind|
35
+ it "ignores #{kind} on #{event}" do
36
+ expect(queue).not_to receive(:remove)
37
+ expect(queue).not_to receive(:update_or_create)
38
+
39
+ headers['X-Contentful-Topic'] = "ContentfulManagement.#{kind}.#{event}"
40
+ request = RequestDummy.new(headers, body)
41
+ subject.respond(request, MockResponse.new).join
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,257 @@
1
+ require 'spec_helper'
2
+
3
+ class WebhookDouble
4
+ attr_reader :id, :space_id, :sys, :fields
5
+ def initialize(id, space_id, sys = {}, fields = {})
6
+ @id = id
7
+ @space_id = space_id
8
+ @sys = sys
9
+ @fields = fields
10
+ end
11
+ end
12
+
13
+ describe Contentful::Scheduler::Queue do
14
+ let(:config) {
15
+ {
16
+ logger: ::Contentful::Scheduler::DEFAULT_LOGGER,
17
+ endpoint: ::Contentful::Scheduler::DEFAULT_ENDPOINT,
18
+ port: ::Contentful::Scheduler::DEFAULT_PORT,
19
+ redis: {
20
+ host: 'localhost',
21
+ port: 12341,
22
+ password: 'foobar'
23
+ },
24
+ spaces: {
25
+ 'foo' => {
26
+ publish_field: 'my_field',
27
+ management_token: 'foo'
28
+ }
29
+ }
30
+ }
31
+ }
32
+
33
+ subject { described_class.instance }
34
+
35
+ before :each do
36
+ allow(Resque).to receive(:redis=)
37
+ described_class.class_variable_set(:@@instance, nil)
38
+ ::Contentful::Scheduler.config = config
39
+ end
40
+
41
+ describe 'singleton' do
42
+ it 'creates an instance if not initialized' do
43
+ queue = described_class.instance
44
+ expect(queue).to be_a described_class
45
+ end
46
+
47
+ it 'reuses same instance' do
48
+ queue = described_class.instance
49
+
50
+ expect(queue).to eq described_class.instance
51
+ end
52
+ end
53
+
54
+ describe 'attributes' do
55
+ it '.config' do
56
+ expect(subject.config).to eq config
57
+ end
58
+ end
59
+
60
+ describe 'instance methods' do
61
+ it '#spaces' do
62
+ expect(subject.spaces).to eq config[:spaces]
63
+ end
64
+
65
+ it '#webhook_publish_field?' do
66
+ expect(subject.webhook_publish_field?(
67
+ WebhookDouble.new('bar', 'foo', {}, {'my_field' => 'something'})
68
+ )).to be_truthy
69
+
70
+ expect(subject.webhook_publish_field?(
71
+ WebhookDouble.new('bar', 'foo', {}, {'not_my_field' => 'something'})
72
+ )).to be_falsey
73
+
74
+ expect(subject.webhook_publish_field?(
75
+ WebhookDouble.new('bar', 'not_foo', {}, {'not_my_field' => 'something'})
76
+ )).to be_falsey
77
+ end
78
+
79
+ it '#webhook_publish_field' do
80
+ expect(subject.webhook_publish_field(
81
+ WebhookDouble.new('bar', 'foo', {}, {'my_field' => 'something'})
82
+ )).to eq 'something'
83
+ end
84
+
85
+ describe '#publish_date' do
86
+ it 'works if date field not localized' do
87
+ expect(subject.publish_date(
88
+ WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'})
89
+ )).to eq DateTime.new(2011, 4, 4, 22, 0, 0).to_time.utc
90
+ end
91
+
92
+ it 'works if date field localized by grabbing first available locale' do
93
+ expect(subject.publish_date(
94
+ WebhookDouble.new('bar', 'foo', {}, {'my_field' => {'en-US': '2011-04-04T22:00:00+00:00'}})
95
+ )).to eq DateTime.new(2011, 4, 4, 22, 0, 0).to_time.utc
96
+
97
+ expect(subject.publish_date(
98
+ WebhookDouble.new('bar', 'foo', {}, {'my_field' => {'en-CA': '2011-04-04T23:00:00Z'}})
99
+ )).to eq DateTime.new(2011, 4, 4, 23, 0, 0).to_time.utc
100
+ end
101
+ end
102
+
103
+ describe '#already_published?' do
104
+ it 'true if webhook publish_date is in past' do
105
+ expect(subject.already_published?(
106
+ WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'})
107
+ )).to be_truthy
108
+ end
109
+
110
+ it 'false if sys.publishedAt is in past' do
111
+ expect(subject.already_published?(
112
+ WebhookDouble.new('bar', 'foo', {'publishedAt' => '2011-04-04T22:00:00+00:00'}, {'my_field' => '2099-04-04T22:00:00+00:00'})
113
+ )).to be_falsey
114
+ end
115
+
116
+ it 'false if sys.publishedAt is not present' do
117
+ expect(subject.already_published?(
118
+ WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2099-04-04T22:00:00+00:00'})
119
+ )).to be_falsey
120
+ end
121
+
122
+ it 'false if sys.publishedAt is present but nil' do
123
+ expect(subject.already_published?(
124
+ WebhookDouble.new('bar', 'foo', {'publishedAt' => nil}, {'my_field' => '2099-04-04T22:00:00+00:00'})
125
+ )).to be_falsey
126
+ end
127
+ end
128
+
129
+ describe '#publishable?' do
130
+ it 'false if webhook space not present in config' do
131
+ expect(subject.publishable?(
132
+ WebhookDouble.new('bar', 'not_foo')
133
+ )).to be_falsey
134
+ end
135
+
136
+ it 'false if publish_field is not found' do
137
+ expect(subject.publishable?(
138
+ WebhookDouble.new('bar', 'foo')
139
+ )).to be_falsey
140
+ end
141
+
142
+ it 'false if publish_field is nil' do
143
+ expect(subject.publishable?(
144
+ WebhookDouble.new('bar', 'foo', {}, {'my_field' => nil})
145
+ )).to be_falsey
146
+ end
147
+
148
+ it 'true if publish_field is populated' do
149
+ expect(subject.publishable?(
150
+ WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'})
151
+ )).to be_truthy
152
+ end
153
+ end
154
+
155
+ describe '#in_queue?' do
156
+ it 'false if not in queue' do
157
+ allow(Resque).to receive(:peek) { [] }
158
+ expect(subject.in_queue?(
159
+ WebhookDouble.new('bar', 'foo')
160
+ )).to be_falsey
161
+ end
162
+
163
+ it 'true if in queue' do
164
+ allow(Resque).to receive(:peek) { [{'args' => ['foo', 'bar']}] }
165
+ expect(subject.in_queue?(
166
+ WebhookDouble.new('bar', 'foo')
167
+ )).to be_truthy
168
+ end
169
+ end
170
+
171
+ describe '#update_or_create' do
172
+ it 'does nothing if webhook is unpublishable' do
173
+ expect(Resque).not_to receive(:enqueue_at)
174
+
175
+ subject.update_or_create(WebhookDouble.new('bar', 'not_foo'))
176
+ end
177
+
178
+ describe 'webhook is new' do
179
+ it 'queues' do
180
+ mock_redis = Object.new
181
+ allow(mock_redis).to receive(:client) { mock_redis }
182
+ allow(mock_redis).to receive(:id) { 'foo' }
183
+ allow(Resque).to receive(:peek) { [] }
184
+ allow(Resque).to receive(:redis) { mock_redis }
185
+
186
+ expect(Resque).to receive(:enqueue_at).with(
187
+ DateTime.strptime('2099-04-04T22:00:00+00:00').to_time.utc,
188
+ ::Contentful::Scheduler::Tasks::Publish,
189
+ 'foo',
190
+ 'bar',
191
+ 'foo'
192
+ ) { true }
193
+
194
+ subject.update_or_create(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2099-04-04T22:00:00+00:00'}))
195
+ end
196
+
197
+ it 'does nothing if already published' do
198
+ allow(Resque).to receive(:peek) { [] }
199
+ expect(Resque).not_to receive(:enqueue_at)
200
+
201
+ subject.update_or_create(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'}))
202
+ end
203
+ end
204
+
205
+ describe 'webhook already in queue' do
206
+ it 'calls remove then queues again' do
207
+ mock_redis = Object.new
208
+ allow(mock_redis).to receive(:client) { mock_redis }
209
+ allow(mock_redis).to receive(:id) { 'foo' }
210
+ allow(Resque).to receive(:redis) { mock_redis }
211
+
212
+ allow(Resque).to receive(:peek) { [{'args' => ['foo', 'bar']}] }
213
+ expect(Resque).to receive(:enqueue_at).with(
214
+ DateTime.strptime('2099-04-04T22:00:00+00:00').to_time.utc,
215
+ ::Contentful::Scheduler::Tasks::Publish,
216
+ 'foo',
217
+ 'bar',
218
+ 'foo'
219
+ ) { true }
220
+ expect(subject).to receive(:remove)
221
+
222
+ subject.update_or_create(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2099-04-04T22:00:00+00:00'}))
223
+ end
224
+
225
+ it 'removes old call if already published' do
226
+ allow(Resque).to receive(:peek) { [{'args' => ['foo', 'bar']}] }
227
+ expect(Resque).not_to receive(:enqueue_at)
228
+ expect(subject).to receive(:remove)
229
+
230
+ subject.update_or_create(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'}))
231
+ end
232
+ end
233
+ end
234
+
235
+ describe '#remove' do
236
+ it 'does nothing if webhook is unpublishable' do
237
+ expect(Resque).not_to receive(:remove_delayed)
238
+
239
+ subject.remove(WebhookDouble.new('bar', 'foo'))
240
+ end
241
+
242
+ it 'does nothing if webhook not in queue' do
243
+ allow(Resque).to receive(:peek) { [] }
244
+ expect(Resque).not_to receive(:remove_delayed)
245
+
246
+ subject.remove(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'}))
247
+ end
248
+
249
+ it 'removes if in queue' do
250
+ allow(Resque).to receive(:peek) { [{'args' => ['foo', 'bar']}] }
251
+ expect(Resque).to receive(:remove_delayed)
252
+
253
+ subject.remove(WebhookDouble.new('bar', 'foo', {}, {'my_field' => '2011-04-04T22:00:00+00:00'}))
254
+ end
255
+ end
256
+ end
257
+ end