kt-delayed_paperclip 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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