kt-delayed_paperclip 3.1.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.
@@ -0,0 +1,30 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "delayed_paperclip/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = %q{kt-delayed_paperclip}
6
+ s.version = DelayedPaperclip::VERSION
7
+
8
+ s.authors = ["Adam Anderson", "Jesse Storimer", "Bert Goethals", "James Gifford", "Scott Carleton"]
9
+ s.summary = %q{Process your Paperclip attachments in the background}
10
+ s.description = %q{Process your Paperclip attachments in the background with ActiveJob}
11
+ s.email = %w{james@jamesrgifford.com scott@artsicle.com}
12
+ s.homepage = %q{https://github.com/adamtao/kt-delayed_paperclip}
13
+
14
+ s.required_ruby_version = ">= 2.0.0"
15
+
16
+ s.add_dependency 'kt-paperclip', "~> 6.4", ">= 6.4.1"
17
+ s.add_dependency 'activejob', ">= 4.2"
18
+
19
+ s.add_development_dependency 'mocha'
20
+ s.add_development_dependency "rspec", '< 3.0'
21
+ s.add_development_dependency 'sqlite3'
22
+ s.add_development_dependency 'appraisal'
23
+ s.add_development_dependency 'rake', '~> 10.5.0'
24
+ s.add_development_dependency 'bundler'
25
+ s.add_development_dependency 'activerecord'
26
+ s.add_development_dependency 'railties'
27
+
28
+ s.files = `git ls-files`.split("\n")
29
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
30
+ end
@@ -0,0 +1 @@
1
+ --- {}
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 4.2.0"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 5.0.0"
6
+
7
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "http://rubygems.org"
4
+
5
+ gem "rails", "~> 5.0.0"
6
+ gem "paperclip", :github => "thoughtbot/paperclip"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,119 @@
1
+ require 'kt-paperclip'
2
+ require 'delayed_paperclip/process_job'
3
+ require 'delayed_paperclip/attachment'
4
+ require 'delayed_paperclip/url_generator'
5
+ require 'delayed_paperclip/railtie' if defined?(Rails)
6
+
7
+ module DelayedPaperclip
8
+ class << self
9
+ def options
10
+ @options ||= {
11
+ :background_job_class => DelayedPaperclip::ProcessJob,
12
+ :url_with_processing => true,
13
+ :processing_image_url => nil,
14
+ :queue => "paperclip"
15
+ }
16
+ end
17
+
18
+ def processor
19
+ options[:background_job_class]
20
+ end
21
+
22
+ def enqueue(instance_klass, instance_id, attachment_name)
23
+ processor.enqueue_delayed_paperclip(instance_klass, instance_id, attachment_name)
24
+ end
25
+
26
+ def process_job(instance_klass, instance_id, attachment_name)
27
+ instance = instance_klass.constantize.unscoped.where(id: instance_id).first
28
+ return if instance.blank?
29
+
30
+ instance.
31
+ send(attachment_name).
32
+ process_delayed!
33
+ end
34
+
35
+ end
36
+
37
+ module Glue
38
+ def self.included(base)
39
+ base.extend(ClassMethods)
40
+ base.send :include, InstanceMethods
41
+ end
42
+ end
43
+
44
+ module ClassMethods
45
+
46
+ def process_in_background(name, options = {})
47
+ # initialize as hash
48
+ paperclip_definitions[name][:delayed] = {}
49
+
50
+ # Set Defaults
51
+ only_process_default = paperclip_definitions[name][:only_process]
52
+ only_process_default ||= []
53
+ {
54
+ :priority => 0,
55
+ :only_process => only_process_default,
56
+ :url_with_processing => DelayedPaperclip.options[:url_with_processing],
57
+ :processing_image_url => DelayedPaperclip.options[:processing_image_url],
58
+ :queue => DelayedPaperclip.options[:queue]
59
+ }.each do |option, default|
60
+ paperclip_definitions[name][:delayed][option] = options.key?(option) ? options[option] : default
61
+ end
62
+
63
+ # Sets callback
64
+ if respond_to?(:after_commit)
65
+ after_commit :enqueue_delayed_processing
66
+ else
67
+ after_save :enqueue_delayed_processing
68
+ end
69
+ end
70
+
71
+ def paperclip_definitions
72
+ if respond_to? :attachment_definitions
73
+ attachment_definitions
74
+ else
75
+ Paperclip::Tasks::Attachments.definitions_for(self)
76
+ end
77
+ end
78
+ end
79
+
80
+ module InstanceMethods
81
+
82
+ # First mark processing
83
+ # then enqueue
84
+ def enqueue_delayed_processing
85
+ mark_enqueue_delayed_processing
86
+
87
+ (@_enqued_for_processing || []).each do |name|
88
+ enqueue_post_processing_for(name)
89
+ end
90
+ @_enqued_for_processing_with_processing = []
91
+ @_enqued_for_processing = []
92
+ end
93
+
94
+ # setting each inididual NAME_processing to true, skipping the ActiveModel dirty setter
95
+ # Then immediately push the state to the database
96
+ def mark_enqueue_delayed_processing
97
+ unless @_enqued_for_processing_with_processing.blank? # catches nil and empty arrays
98
+ updates = @_enqued_for_processing_with_processing.collect{|n| "#{n}_processing = :true" }.join(", ")
99
+ updates = ActiveRecord::Base.send(:sanitize_sql_array, [updates, {:true => true}])
100
+ self.class.unscoped.where(:id => self.id).update_all(updates)
101
+ end
102
+ end
103
+
104
+ def enqueue_post_processing_for name
105
+ DelayedPaperclip.enqueue(self.class.name, read_attribute(:id), name.to_sym)
106
+ end
107
+
108
+ def prepare_enqueueing_for name
109
+ if self.attributes.has_key? "#{name}_processing"
110
+ write_attribute("#{name}_processing", true)
111
+ @_enqued_for_processing_with_processing ||= []
112
+ @_enqued_for_processing_with_processing << name
113
+ end
114
+
115
+ @_enqued_for_processing ||= []
116
+ @_enqued_for_processing << name
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,89 @@
1
+ module DelayedPaperclip
2
+ module Attachment
3
+ attr_accessor :job_is_processing
4
+
5
+ def delayed_options
6
+ options[:delayed]
7
+ end
8
+
9
+ # Attr accessor in Paperclip
10
+ def post_processing
11
+ !delay_processing? || split_processing?
12
+ end
13
+
14
+ def post_processing=(value)
15
+ @post_processing_with_delay = value
16
+ end
17
+
18
+ # if nil, returns whether it has delayed options
19
+ # if set, then it returns
20
+ def delay_processing?
21
+ if @post_processing_with_delay.nil?
22
+ !!delayed_options
23
+ else
24
+ !@post_processing_with_delay
25
+ end
26
+ end
27
+
28
+ def split_processing?
29
+ options[:only_process] && delayed_options &&
30
+ options[:only_process] != delayed_only_process
31
+ end
32
+
33
+ def processing?
34
+ column_name = :"#{@name}_processing?"
35
+ @instance.respond_to?(column_name) && @instance.send(column_name)
36
+ end
37
+
38
+ def processing_style?(style)
39
+ return false if !processing?
40
+
41
+ !split_processing? || delayed_only_process.include?(style)
42
+ end
43
+
44
+ def delayed_only_process
45
+ only_process = delayed_options.fetch(:only_process, []).dup
46
+ only_process = only_process.call(self) if only_process.respond_to?(:call)
47
+ only_process.map(&:to_sym)
48
+ end
49
+
50
+ def process_delayed!
51
+ self.job_is_processing = true
52
+ self.post_processing = true
53
+ reprocess!(*delayed_only_process)
54
+ self.job_is_processing = false
55
+ update_processing_column
56
+ end
57
+
58
+ def processing_image_url
59
+ processing_image_url = delayed_options[:processing_image_url]
60
+ processing_image_url = processing_image_url.call(self) if processing_image_url.respond_to?(:call)
61
+ processing_image_url
62
+ end
63
+
64
+ def save
65
+ was_dirty = @dirty
66
+
67
+ super.tap do
68
+ if delay_processing? && was_dirty
69
+ instance.prepare_enqueueing_for name
70
+ end
71
+ end
72
+ end
73
+
74
+ def reprocess_without_delay!(*style_args)
75
+ @post_processing_with_delay = true
76
+ reprocess!(*style_args)
77
+ end
78
+
79
+ private
80
+
81
+ def update_processing_column
82
+ if instance.respond_to?(:"#{name}_processing?")
83
+ instance.send("#{name}_processing=", false)
84
+ instance.class.unscoped.where(instance.class.primary_key => instance.id).update_all({ "#{name}_processing" => false })
85
+ end
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,14 @@
1
+ require "active_job"
2
+
3
+ module DelayedPaperclip
4
+ class ProcessJob < ActiveJob::Base
5
+ def self.enqueue_delayed_paperclip(instance_klass, instance_id, attachment_name)
6
+ queue_name = instance_klass.constantize.paperclip_definitions[attachment_name][:delayed][:queue]
7
+ set(:queue => queue_name).perform_later(instance_klass, instance_id, attachment_name.to_s)
8
+ end
9
+
10
+ def perform(instance_klass, instance_id, attachment_name)
11
+ DelayedPaperclip.process_job(instance_klass, instance_id, attachment_name.to_sym)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,27 @@
1
+ require "paperclip"
2
+ require "delayed_paperclip"
3
+
4
+ module DelayedPaperclip
5
+ # On initialzation, include DelayedPaperclip
6
+ class Railtie < Rails::Railtie
7
+ initializer "delayed_paperclip.insert_into_active_record" do |app|
8
+ ActiveSupport.on_load :active_record do
9
+ DelayedPaperclip::Railtie.insert
10
+ end
11
+
12
+ if app.config.respond_to?(:delayed_paperclip_defaults)
13
+ DelayedPaperclip.options.merge!(app.config.delayed_paperclip_defaults)
14
+ end
15
+ end
16
+ end
17
+
18
+ class Railtie
19
+ # Glue includes DelayedPaperclip Class Methods and Instance Methods into ActiveRecord
20
+ # Attachment and URL Generator extends Paperclip
21
+ def self.insert
22
+ ActiveRecord::Base.send(:include, DelayedPaperclip::Glue)
23
+ Paperclip::Attachment.prepend(DelayedPaperclip::Attachment)
24
+ Paperclip::Attachment.default_options[:url_generator] = DelayedPaperclip::UrlGenerator
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ require 'uri'
2
+ require 'paperclip/url_generator'
3
+
4
+ module DelayedPaperclip
5
+ class UrlGenerator < ::Paperclip::UrlGenerator
6
+ def initialize(attachment, _compatibility = nil)
7
+ @attachment = attachment
8
+ @attachment_options = attachment.options
9
+ end
10
+
11
+ def for(style_name, options)
12
+ most_appropriate_url = @attachment.processing_style?(style_name) ? most_appropriate_url(style_name) : most_appropriate_url()
13
+ timestamp_as_needed(
14
+ escape_url_as_needed(
15
+ @attachment_options[:interpolator].interpolate(most_appropriate_url, @attachment, style_name),
16
+ options
17
+ ),
18
+ options)
19
+ end
20
+
21
+ # This method is a mess
22
+ def most_appropriate_url(style = nil)
23
+ if @attachment.processing_style?(style)
24
+ if @attachment.original_filename.nil? || delayed_default_url?(style)
25
+
26
+ if @attachment.delayed_options.nil? ||
27
+ @attachment.processing_image_url.nil? ||
28
+ !@attachment.processing?
29
+ default_url
30
+ else
31
+ @attachment.processing_image_url
32
+ end
33
+
34
+ else
35
+ @attachment_options[:url]
36
+ end
37
+ else
38
+ super()
39
+ end
40
+ end
41
+
42
+ def timestamp_possible?
43
+ delayed_default_url? ? false : super
44
+ end
45
+
46
+ def delayed_default_url?(style = nil)
47
+ return false if @attachment.job_is_processing
48
+ return false if @attachment.dirty?
49
+ return false if not @attachment.delayed_options.try(:[], :url_with_processing)
50
+ return false if not processing?(style)
51
+ true
52
+ end
53
+
54
+ private
55
+
56
+ def processing?(style)
57
+ return true if @attachment.processing?
58
+ return @attachment.processing_style?(style) if style
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,3 @@
1
+ module DelayedPaperclip
2
+ VERSION = "3.1.0"
3
+ end
@@ -0,0 +1,2 @@
1
+ require 'delayed_paperclip'
2
+
@@ -0,0 +1,345 @@
1
+ require 'spec_helper'
2
+
3
+ describe DelayedPaperclip::Attachment do
4
+ before :each do
5
+ reset_dummy(dummy_options)
6
+ end
7
+
8
+ let(:dummy_options) { {} }
9
+ let(:dummy) { Dummy.create }
10
+
11
+ describe "#delayed_options" do
12
+ it "returns the specific options for delayed paperclip" do
13
+ expect(dummy.image.delayed_options).to eq({
14
+ :priority => 0,
15
+ :only_process => [],
16
+ :url_with_processing => true,
17
+ :processing_image_url => nil,
18
+ :queue => "paperclip"
19
+ })
20
+ end
21
+ end
22
+
23
+ describe "#post_processing_with_delay" do
24
+ it "is true if delay_processing? is false" do
25
+ dummy.image.stubs(:delay_processing?).returns false
26
+ dummy.image.post_processing.should be_truthy
27
+ end
28
+
29
+ it "is false if delay_processing? is true" do
30
+ dummy.image.stubs(:delay_processing?).returns true
31
+ dummy.image.post_processing.should be_falsey
32
+ end
33
+
34
+ context "on a non-delayed image" do
35
+ let(:dummy_options) { { with_processed: false } }
36
+
37
+ it "is false if delay_processing? is true" do
38
+ dummy.image.stubs(:delay_processing?).returns true
39
+ dummy.image.post_processing.should be_falsey
40
+ end
41
+ end
42
+ end
43
+
44
+ describe "#delay_processing?" do
45
+ it "returns delayed_options existence if post_processing is nil" do
46
+ dummy.image.post_processing = nil
47
+ dummy.image.delay_processing?.should be_truthy
48
+ end
49
+
50
+ it "returns inverse of post_processing if it's set" do
51
+ dummy.image.post_processing = true
52
+ dummy.image.delay_processing?.should be_falsey
53
+ end
54
+ end
55
+
56
+ describe "#processing?" do
57
+ it "delegates to the dummy instance" do
58
+ dummy.expects(:image_processing?)
59
+ dummy.image.processing?
60
+ end
61
+
62
+ context "without a processing column" do
63
+ let(:dummy_options) { { with_processed: false } }
64
+
65
+ it "returns false" do
66
+ expect(dummy.image.processing?).to be_falsey
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "#processing_style?" do
72
+ let(:style) { :background }
73
+ let(:processing_style?) { dummy.image.processing_style?(style) }
74
+
75
+ context "without a processing column" do
76
+ let(:dummy_options) { { with_processed: true, process_column: false } }
77
+
78
+ specify { expect(processing_style?).to be_falsey }
79
+ end
80
+
81
+ context "with a processing column" do
82
+ context "when not processing" do
83
+ before { dummy.image_processing = false }
84
+
85
+ specify { expect(processing_style?).to be_falsey }
86
+ end
87
+
88
+ context "when processing" do
89
+ before { dummy.image_processing = true }
90
+
91
+ context "when not split processing" do
92
+ specify { expect(processing_style?).to be_truthy }
93
+ end
94
+
95
+ context "when split processing" do
96
+ context "when delayed :only_process is an Array" do
97
+ let(:dummy_options) { {
98
+ paperclip: {
99
+ styles: {
100
+ online: "400x400x",
101
+ background: "600x600x"
102
+ },
103
+ only_process: [:online]
104
+ },
105
+
106
+ only_process: [:background]
107
+ }}
108
+
109
+ specify { expect(processing_style?).to be }
110
+ end
111
+
112
+ context "when delayed :only_process is callable" do
113
+ let(:dummy_options) { {
114
+ paperclip: {
115
+ styles: {
116
+ online: "400x400x",
117
+ background: "600x600x"
118
+ },
119
+ only_process: [:online]
120
+ },
121
+
122
+ only_process: lambda { |a| [:background] }
123
+ }}
124
+
125
+ specify { expect(processing_style?).to be }
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ describe "#delayed_only_process" do
133
+ context "without only_process options" do
134
+ it "returns []" do
135
+ expect(dummy.image.delayed_only_process).to eq []
136
+ end
137
+ end
138
+
139
+ context "with only_process options" do
140
+ before :each do
141
+ reset_dummy(paperclip: { only_process: [:small, :large] } )
142
+ end
143
+
144
+ it "returns [:small, :large]" do
145
+ expect(dummy.image.delayed_only_process).to eq [:small, :large]
146
+ end
147
+ end
148
+
149
+ context "with only_process set with callable" do
150
+ before :each do
151
+ reset_dummy(paperclip: { only_process: lambda { |a| [:small, :large] } } )
152
+ end
153
+
154
+ # Enable when https://github.com/thoughtbot/paperclip/pull/2289 is resolved
155
+ xit "returns [:small, :large]" do
156
+ expect(dummy.image.delayed_only_process).to eq [:small, :large]
157
+ end
158
+ end
159
+ end
160
+
161
+ describe "#process_delayed!" do
162
+ it "sets job_is_processing to true" do
163
+ dummy.image.expects(:job_is_processing=).with(true).once
164
+ dummy.image.expects(:job_is_processing=).with(false).once
165
+ dummy.image.process_delayed!
166
+ end
167
+
168
+ it "sets post_processing to true" do
169
+ dummy.image.expects(:post_processing=).with(true).once
170
+ dummy.image.process_delayed!
171
+ end
172
+
173
+ context "without only_process options" do
174
+ it "calls reprocess!" do
175
+ dummy.image.expects(:reprocess!)
176
+ dummy.image.process_delayed!
177
+ end
178
+ end
179
+
180
+ context "with only_process options" do
181
+ before :each do
182
+ reset_dummy(paperclip: { only_process: [:small, :large] } )
183
+ end
184
+
185
+ it "calls reprocess! with options" do
186
+ dummy.image.expects(:reprocess!).with(:small, :large)
187
+ dummy.image.process_delayed!
188
+ end
189
+ end
190
+
191
+ context "with only_process set with callable" do
192
+ before :each do
193
+ reset_dummy(paperclip: { only_process: lambda { |a| [:small, :large] } } )
194
+ end
195
+
196
+ # Enable when https://github.com/thoughtbot/paperclip/pull/2289 is resolved
197
+ xit "calls reprocess! with options" do
198
+ dummy.image.expects(:reprocess!).with(:small, :large)
199
+ dummy.image.process_delayed!
200
+ end
201
+ end
202
+ end
203
+
204
+ describe "#processing_image_url" do
205
+ context "no url" do
206
+ it "returns nil" do
207
+ dummy.image.processing_image_url.should be_nil
208
+ end
209
+ end
210
+
211
+ context "static url" do
212
+ before :each do
213
+ reset_dummy(:processing_image_url => "/foo/bar.jpg")
214
+ end
215
+
216
+ it "returns given url" do
217
+ dummy.image.processing_image_url.should == "/foo/bar.jpg"
218
+ end
219
+ end
220
+
221
+ context "proc" do
222
+ before :each do
223
+ reset_dummy(:processing_image_url => proc { "Hello/World" } )
224
+ end
225
+
226
+ it "returns evaluates proc" do
227
+ dummy.image.processing_image_url.should == "Hello/World"
228
+ end
229
+ end
230
+ end
231
+
232
+ describe "#update_processing_column" do
233
+ it "updates the column to false" do
234
+ dummy.update_attribute(:image_processing, true)
235
+
236
+ dummy.image.send(:update_processing_column)
237
+
238
+ dummy.reload.image_processing.should be_falsey
239
+ end
240
+
241
+ context 'with a default scope on the model excluding the instance' do
242
+ let(:dummy_options) do
243
+ { :default_scope => lambda { Dummy.where(hidden: false) } }
244
+ end
245
+
246
+ let!(:dummy) { Dummy.create(hidden: true) }
247
+
248
+ specify { Dummy.count.should be 0 }
249
+ specify { Dummy.unscoped.count.should be 1 }
250
+
251
+ it "ignores the default scope and updates the column to false" do
252
+ dummy.update_attribute(:image_processing, true)
253
+ dummy.image.send(:update_processing_column)
254
+ dummy.reload.image_processing.should be_falsey
255
+ end
256
+ end
257
+ end
258
+
259
+ describe "#save" do
260
+ context "delay processing and it was dirty" do
261
+ before :each do
262
+ dummy.image.stubs(:delay_processing?).returns true
263
+ dummy.image.instance_variable_set(:@dirty, true)
264
+ end
265
+
266
+ it "prepares the enqueing" do
267
+ dummy.expects(:prepare_enqueueing_for).with(:image)
268
+ dummy.image.save
269
+ end
270
+ end
271
+
272
+ context "without dirty or delay_processing" do
273
+ it "does not prepare_enqueueing" do
274
+ dummy.expects(:prepare_enqueueing_for).with(:image).never
275
+ dummy.image.save
276
+ end
277
+ end
278
+ end
279
+
280
+ describe "#reprocess_without_delay!" do
281
+ it "sets post post_processing_with_delay and reprocesses with given args" do
282
+ dummy.image.expects(:reprocess!).with(:small)
283
+ dummy.image.reprocess_without_delay!(:small)
284
+ dummy.image.instance_variable_get(:@post_processing_with_delay).should == true
285
+ end
286
+ end
287
+
288
+ describe "#split_processing?" do
289
+ let(:split_processing?) { dummy.image.split_processing? }
290
+
291
+ let(:paperclip_styles) { {
292
+ online: "400x400x",
293
+ background: "600x600x"
294
+ } }
295
+
296
+ context ":only_process option is set on attachment" do
297
+ let(:dummy_options) { {
298
+ paperclip: {
299
+ styles: paperclip_styles,
300
+ only_process: [:online]
301
+ },
302
+
303
+ only_process: delayed_only_process
304
+ }}
305
+
306
+ context "processing different styles in background" do
307
+ context "when delayed :only_process is an Array" do
308
+ let(:delayed_only_process) { [:background] }
309
+
310
+ specify { expect(split_processing?).to be true }
311
+ end
312
+
313
+ context "when delayed :only_process is callable" do
314
+ let(:delayed_only_process) { lambda { |a| [:background] } }
315
+
316
+ specify { expect(split_processing?).to be true }
317
+ end
318
+ end
319
+
320
+ context "processing same styles in background" do
321
+ context "when delayed :only_process is an Array" do
322
+ let(:delayed_only_process) { [:online] }
323
+
324
+ specify { expect(split_processing?).to be false }
325
+ end
326
+
327
+ context "when delayed :only_process is callable" do
328
+ let(:delayed_only_process) { lambda { |a| [:online] } }
329
+
330
+ specify { expect(split_processing?).to be false }
331
+ end
332
+ end
333
+ end
334
+
335
+ context ":only_process option is not set on attachment" do
336
+ let(:dummy_options) { {
337
+ paperclip: {
338
+ styles: paperclip_styles
339
+ }
340
+ }}
341
+
342
+ specify { expect(split_processing?).to be false }
343
+ end
344
+ end
345
+ end