leifcr-refile 0.6.3 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: f781b9f6d4404dc29d2b92347798a84e7208a8ad
4
- data.tar.gz: d3d68577da41b921cfa349aafc142bb8d5d5550e
2
+ SHA256:
3
+ metadata.gz: 207442fab5c7ffc212b6a306de4750f4c400c616c6f04bcca3d497b9cdc95cbb
4
+ data.tar.gz: '09ec1056de2707eeee3586a6b0bcc4ab67a88adcd4d4ba81ab14d2d63a64881d'
5
5
  SHA512:
6
- metadata.gz: d605f5943011f2b8258b335b045f949f58aabf4eb66a4f1e2f3001387d682d3c60f0580543e22b8d426695b959def093858a1cb904a2915b98277c90f0d80748
7
- data.tar.gz: 55e9254dc55bfccf60caa620bf1afe3257b4f630e9d459c826bc1ce88e0fecf137fa721bf13fcddc53da8a1223781648d0dd8dcdfd5b2359c5f8c2e3c3749b2e
6
+ metadata.gz: 0d63019644aa26fbfae2998559b4c43fceb148fd018a71bd78d1a8bb59b49f7db62c1190f6f2c01735f3fdb34659876594444003d512fb66d863a5517a314c09
7
+ data.tar.gz: 1415e64857f076ff3ed80c419560661e03d9a827c32e4f742c844b6289a529299101060f8fef3a4fa0b8a0f4d8f0bdcee109c895208fd26b972a008006b11888
@@ -115,7 +115,10 @@
115
115
  return data;
116
116
  });
117
117
  if(!input.multiple) dataObj = dataObj[0];
118
- if(metadataField) metadataField.value = JSON.stringify(dataObj);
118
+ if(metadataField) {
119
+ metadataField.value = JSON.stringify(dataObj);
120
+ metadataField.removeAttribute('disabled');
121
+ };
119
122
 
120
123
  input.removeAttribute("name");
121
124
  }
data/lib/refile.rb CHANGED
@@ -1,13 +1,15 @@
1
1
  require "uri"
2
2
  require "fileutils"
3
3
  require "tempfile"
4
- require "rest_client"
5
4
  require "logger"
6
5
  require "mime/types"
7
6
 
8
7
  module Refile
9
8
  # @api private
10
- class Invalid < StandardError; end
9
+ class Error < StandardError; end
10
+
11
+ # @api private
12
+ class Invalid < Error; end
11
13
 
12
14
  # @api private
13
15
  class InvalidID < Invalid; end
@@ -18,6 +20,12 @@ module Refile
18
20
  # @api private
19
21
  class InvalidFile < Invalid; end
20
22
 
23
+ # Raised when the given URL couldn't be parsed.
24
+ class InvalidUrl < Error; end
25
+
26
+ # Raised when the given URL redirects more than allowed.
27
+ class TooManyRedirects < Error; end
28
+
21
29
  # @api private
22
30
  class Confirm < StandardError
23
31
  def message
@@ -489,8 +497,10 @@ module Refile
489
497
  require "refile/type"
490
498
  require "refile/backend_macros"
491
499
  require "refile/attachment_definition"
500
+ require "refile/download"
492
501
  require "refile/attacher"
493
502
  require "refile/attachment"
503
+ require "refile/attachment/multiple_attachments"
494
504
  require "refile/random_hasher"
495
505
  require "refile/file"
496
506
  require "refile/custom_logger"
@@ -73,6 +73,7 @@ module Refile
73
73
  end
74
74
 
75
75
  def set(value)
76
+ self.remove = false
76
77
  case value
77
78
  when nil then self.remove = true
78
79
  when String, Hash then retrieve!(value)
@@ -105,21 +106,20 @@ module Refile
105
106
 
106
107
  def download(url)
107
108
  unless url.to_s.empty?
108
- response = RestClient::Request.new(method: :get, url: url, raw_response: true).execute
109
+ download = Refile::Download.new(url)
109
110
  @metadata = {
110
- size: response.file.size,
111
- filename: URI.parse(url).path.split("/").last,
112
- content_type: response.headers[:content_type]
111
+ size: download.size,
112
+ filename: download.original_filename,
113
+ content_type: download.content_type
113
114
  }
114
115
  if valid?
115
- response.file.open if response.file.closed? # https://github.com/refile/refile/pull/210
116
- @metadata[:id] = cache.upload(response.file).id
116
+ @metadata[:id] = cache.upload(download.io).id
117
117
  write_metadata
118
118
  elsif @definition.raise_errors?
119
119
  raise Refile::Invalid, @errors.join(", ")
120
120
  end
121
121
  end
122
- rescue RestClient::Exception
122
+ rescue Refile::Error
123
123
  @errors = [:download_failed]
124
124
  raise if @definition.raise_errors?
125
125
  end
@@ -104,5 +104,63 @@ module Refile
104
104
 
105
105
  include mod
106
106
  end
107
+
108
+ # Macro which generates accessors in pure Ruby classes for assigning
109
+ # multiple attachments at once. This is primarily useful together with
110
+ # multiple file uploads. There is also an Active Record version of
111
+ # this macro.
112
+ #
113
+ # The name of the generated accessors will be the name of the association
114
+ # (represented by an attribute accessor) and the name of the attachment in
115
+ # the associated class. So if a `Post` accepts attachments for `images`, and
116
+ # the attachment in the `Image` class is named `file`, then the accessors will
117
+ # be named `images_files`.
118
+ #
119
+ # @example in associated class
120
+ # class Document
121
+ # extend Refile::Attachment
122
+ # attr_accessor :file_id
123
+ #
124
+ # attachment :file
125
+ #
126
+ # def initialize(attributes = {})
127
+ # self.file = attributes[:file]
128
+ # end
129
+ # end
130
+ #
131
+ # @example in class
132
+ # class Post
133
+ # extend Refile::Attachment
134
+ # include ActiveModel::Model
135
+ #
136
+ # attr_accessor :documents
137
+ #
138
+ # accepts_attachments_for :documents, accessor_prefix: 'documents_files', collection_class: Document
139
+ #
140
+ # def initialize(attributes = {})
141
+ # @documents = attributes[:documents] || []
142
+ # end
143
+ # end
144
+ #
145
+ # @example in form
146
+ # <%= form_for @post do |form| %>
147
+ # <%= form.attachment_field :documents_files, multiple: true %>
148
+ # <% end %>
149
+ #
150
+ # @param [Symbol] collection_name Name of the association
151
+ # @param [Class] collection_class Associated class
152
+ # @param [String] accessor_prefix Name of the generated accessors
153
+ # @param [Symbol] attachment Name of the attachment in the associated class
154
+ # @param [Boolean] append If true, new files are appended instead of replacing the entire list of associated classes.
155
+ # @return [void]
156
+ def accepts_attachments_for(collection_name, collection_class:, accessor_prefix:, attachment: :file, append: false)
157
+ include MultipleAttachments.new(
158
+ collection_name,
159
+ collection_class: collection_class,
160
+ name: accessor_prefix,
161
+ attachment: attachment,
162
+ append: append
163
+ )
164
+ end
107
165
  end
108
166
  end
@@ -47,8 +47,9 @@ module Refile
47
47
  end
48
48
  end
49
49
 
50
- # Macro which generates accessors for assigning multiple attachments at
51
- # once. This is primarily useful together with multiple file uploads.
50
+ # Macro which generates accessors in Active Record classes for assigning
51
+ # multiple attachments at once. This is primarily useful together with
52
+ # multiple file uploads. There is also a pure Ruby version of this macro.
52
53
  #
53
54
  # The name of the generated accessors will be the name of the association
54
55
  # and the name of the attachment in the associated model. So if a `Post`
@@ -71,55 +72,29 @@ module Refile
71
72
  # <%= form.attachment_field :images_files, multiple: true %>
72
73
  # <% end %>
73
74
  #
74
- # @param [Symbol] association_name Name of the association
75
- # @param [Symbol] attachment Name of the attachment in the associated model
76
- # @param [Symbol] append If true, new files are appended instead of replacing the entire list of associated models.
75
+ # @param [Symbol] association_name Name of the association
76
+ # @param [Symbol] attachment Name of the attachment in the associated model
77
+ # @param [Boolean] append If true, new files are appended instead of replacing the entire list of associated models.
77
78
  # @return [void]
78
79
  def accepts_attachments_for(association_name, attachment: :file, append: false)
79
80
  association = reflect_on_association(association_name)
80
81
  attachment_pluralized = attachment.to_s.pluralize
81
82
  name = "#{association_name}_#{attachment_pluralized}"
83
+ collection_class = association && association.klass
82
84
 
83
- mod = Module.new do
84
- define_method :"#{name}_attachment_definition" do
85
- association.klass.send("#{attachment}_attachment_definition")
86
- end
85
+ options = {
86
+ collection_class: collection_class,
87
+ name: name,
88
+ attachment: attachment,
89
+ append: append
90
+ }
87
91
 
88
- define_method(:method_missing) do |method|
92
+ mod = MultipleAttachments.new association_name, **options do
93
+ define_method(:method_missing) do |method, *args|
89
94
  if method == attachment_pluralized.to_sym
90
95
  raise NoMethodError, "wrong association name #{method}, use like this #{name}"
91
96
  else
92
- super(method)
93
- end
94
- end
95
-
96
- define_method :"#{name}_data" do
97
- if send(association_name).all? { |record| record.send("#{attachment}_attacher").valid? }
98
- send(association_name).map(&:"#{attachment}_data").select(&:present?)
99
- end
100
- end
101
-
102
- define_method :"#{name}" do
103
- send(association_name).map(&attachment)
104
- end
105
-
106
- define_method :"#{name}=" do |files|
107
- cache, files = files.partition { |file| file.is_a?(String) }
108
-
109
- cache = Refile.parse_json(cache.first)
110
-
111
- if not append and (files.present? or cache.present?)
112
- send("#{association_name}=", [])
113
- end
114
-
115
- if files.empty? and cache.present?
116
- cache.select(&:present?).each do |file|
117
- send(association_name).build(attachment => file.to_json)
118
- end
119
- else
120
- files.select(&:present?).each do |file|
121
- send(association_name).build(attachment => file)
122
- end
97
+ super(method, *args)
123
98
  end
124
99
  end
125
100
  end
@@ -0,0 +1,54 @@
1
+ module Refile
2
+ module Attachment
3
+ # Builds a module to be used by "accepts_attachments_for"
4
+ #
5
+ # @api private
6
+ module MultipleAttachments
7
+ def self.new(collection_name, collection_class:, name:, attachment:, append:, &block)
8
+ Module.new do
9
+ define_method :"#{name}_attachment_definition" do
10
+ collection_class.send("#{attachment}_attachment_definition")
11
+ end
12
+
13
+ define_method :"#{name}_data" do
14
+ collection = send(collection_name)
15
+
16
+ all_attachers_valid = collection.all? do |record|
17
+ record.send("#{attachment}_attacher").valid?
18
+ end
19
+
20
+ collection.map(&:"#{attachment}_data") if all_attachers_valid
21
+ end
22
+
23
+ define_method :"#{name}" do
24
+ send(collection_name).map(&attachment)
25
+ end
26
+
27
+ define_method :"#{name}=" do |files|
28
+ cache, files = [files].flatten.partition { |file| file.is_a?(String) }
29
+ cache = Refile.parse_json(cache.first) || []
30
+ cache = cache.reject(&:empty?)
31
+ files = files.compact
32
+
33
+ if not append and (!files.empty? or !cache.empty?)
34
+ send("#{collection_name}=", [])
35
+ end
36
+
37
+ collection = send(collection_name)
38
+
39
+ if files.empty? and !cache.empty?
40
+ cache.each do |file|
41
+ collection << collection_class.new(attachment => file.to_json)
42
+ end
43
+ else
44
+ files.each do |file|
45
+ collection << collection_class.new(attachment => file)
46
+ end
47
+ end
48
+ end
49
+ module_eval(&block) if block_given?
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,98 @@
1
+ require "open-uri"
2
+ require "forwardable"
3
+ require "cgi"
4
+
5
+ module Refile
6
+ # This class downloads a given URL and returns its IO, size, content type and
7
+ # original file name.
8
+ #
9
+ # Usage:
10
+ #
11
+ # download = Refile::Download.new('http://example.com/my/data.bin')
12
+ # download.io
13
+ # #=> #<StringIO:0x00007fdcb3932fc8 ...>
14
+ # download.size
15
+ # #=> 389620
16
+ # download.content_type
17
+ # #=> "application/octet-stream"
18
+ # download.original_file_name
19
+ # #=> "data.bin"
20
+ class Download
21
+ OPTIONS = {
22
+ "User-Agent" => "Refile/#{Refile::VERSION}",
23
+ open_timeout: 30,
24
+ read_timeout: 30,
25
+ redirect: false
26
+ }.freeze
27
+
28
+ extend Forwardable
29
+ def_delegators :@io, :size, :content_type
30
+
31
+ attr_reader :io, :original_filename
32
+
33
+ def initialize(uri)
34
+ @io = download(uri)
35
+ @original_filename = extract_original_filename
36
+ end
37
+
38
+ private
39
+
40
+ def download(uri)
41
+ uri = ensure_uri(uri)
42
+ follows_remaining = 10
43
+
44
+ begin
45
+ uri.open(OPTIONS)
46
+ rescue OpenURI::HTTPRedirect => exception
47
+ raise Refile::TooManyRedirects if follows_remaining.zero?
48
+
49
+ uri = ensure_uri(exception.uri)
50
+ follows_remaining -= 1
51
+
52
+ retry
53
+ rescue OpenURI::HTTPError => exception
54
+ if exception.message.include?("(Invalid Location URI)")
55
+ raise Refile::InvalidUrl, "Invalid Redirect URI: #{response["Location"]}"
56
+ end
57
+
58
+ raise exception
59
+ end
60
+ end
61
+
62
+ def ensure_uri(url)
63
+ begin
64
+ uri = URI(url)
65
+ rescue URI::InvalidURIError
66
+ raise Refile::InvalidUrl, "Invalid URI: #{uri.inspect}"
67
+ end
68
+
69
+ unless uri.is_a?(URI::HTTP)
70
+ raise Refile::InvalidUrl, "URL scheme needs to be http or https: #{uri}"
71
+ end
72
+
73
+ uri
74
+ end
75
+
76
+ def extract_original_filename
77
+ filename_from_content_disposition || filename_from_path
78
+ end
79
+
80
+ def filename_from_content_disposition
81
+ content_disposition = @io.meta["content-disposition"].to_s
82
+
83
+ escaped_filename =
84
+ content_disposition[/filename\*=UTF-8''(\S+)/, 1] ||
85
+ content_disposition[/filename="([^"]*)"/, 1] ||
86
+ content_disposition[/filename=(\S+)/, 1]
87
+
88
+ filename = CGI.unescape(escaped_filename.to_s)
89
+
90
+ filename unless filename.empty?
91
+ end
92
+
93
+ def filename_from_path
94
+ filename = @io.base_uri.path.split("/").last
95
+ CGI.unescape(filename) if filename
96
+ end
97
+ end
98
+ end
@@ -106,10 +106,13 @@ module Refile
106
106
  options[:data] ||= {}
107
107
  options[:data][:reference] ||= SecureRandom.hex
108
108
 
109
+ attacher_value = object.send("#{method}_data")
110
+
109
111
  hidden_options = {
110
112
  multiple: options[:multiple],
111
- value: object.send("#{method}_data").try(:to_json),
113
+ value: attacher_value.try(:to_json),
112
114
  object: object,
115
+ disabled: attacher_value.blank?,
113
116
  id: nil,
114
117
  data: { reference: options[:data][:reference] }
115
118
  }
@@ -1,3 +1,3 @@
1
1
  module Refile
2
- VERSION = "0.6.3"
2
+ VERSION = "0.7.0"
3
3
  end
@@ -8,7 +8,7 @@ ActiveRecord::Base.establish_connection(
8
8
  verbosity: "quiet"
9
9
  )
10
10
 
11
- class TestMigration < ActiveRecord::Migration
11
+ class TestMigration < ActiveRecord::Migration[5.0]
12
12
  def self.up
13
13
  create_table :posts, force: true do |t|
14
14
  t.integer :user_id
@@ -1,5 +1,6 @@
1
1
  require "refile/active_record_helper"
2
2
  require "refile/attachment/active_record"
3
+ require_relative "../support/accepts_attachments_for_shared_examples"
3
4
 
4
5
  describe Refile::ActiveRecord::Attachment do
5
6
  let(:options) { {} }
@@ -273,6 +274,18 @@ describe Refile::ActiveRecord::Attachment do
273
274
  expect(Refile.store.read(post.document.id)).to eq("hello")
274
275
  expect(post.document.id).not_to be eq old_document.id
275
276
  end
277
+
278
+ it "replaces a attachment which was nil" do
279
+ post = klass.new
280
+ post.document = nil
281
+ post.save
282
+
283
+ post.document = Refile::FileDouble.new("hello")
284
+ post.save
285
+
286
+ expect(Refile.store.read(post.document.id)).to eq("hello")
287
+ expect(post.document).not_to be_nil
288
+ end
276
289
  end
277
290
 
278
291
  describe "#destroy" do
@@ -339,119 +352,31 @@ describe Refile::ActiveRecord::Attachment do
339
352
  "Document"
340
353
  end
341
354
 
342
- attachment :file
355
+ attachment :file, type: :image
343
356
  end
344
357
  end
345
358
 
346
359
  let(:post) { post_class.new }
347
360
 
348
- describe "#:association_:name" do
361
+ it_should_behave_like "accepts_attachments_for"
362
+
363
+ it "shouldn't raise error on setter method_missing" do
364
+ expect { post.creator = nil }.to_not raise_error(ArgumentError)
365
+ end
366
+
367
+ context "when a wrong association is called" do
349
368
  let(:wrong_method) { "files" }
350
369
  let(:wrong_association_message) do
351
370
  "wrong association name #{wrong_method}, use like this documents_files"
352
371
  end
353
372
 
354
- it "returns a friendly error message for wrong association name" do
373
+ it "returns a friendly error message" do
355
374
  expect { post.send(wrong_method) }.to raise_error(wrong_association_message)
356
375
  end
357
376
 
358
377
  it "return method missing" do
359
378
  expect { post.foo }.to_not raise_error(wrong_association_message)
360
379
  end
361
-
362
- it "builds records from assigned files" do
363
- post.documents_files = [Refile::FileDouble.new("hello"), Refile::FileDouble.new("world")]
364
- expect(post.documents[0].file.read).to eq("hello")
365
- expect(post.documents[1].file.read).to eq("world")
366
- expect(post.documents.size).to eq(2)
367
- end
368
-
369
- it "builds records from cache" do
370
- post.documents_files = [
371
- [
372
- { id: Refile.cache.upload(Refile::FileDouble.new("hello")).id },
373
- { id: Refile.cache.upload(Refile::FileDouble.new("world")).id }
374
- ].to_json
375
- ]
376
- expect(post.documents[0].file.read).to eq("hello")
377
- expect(post.documents[1].file.read).to eq("world")
378
- expect(post.documents.size).to eq(2)
379
- end
380
-
381
- it "prefers newly uploaded files over cache" do
382
- post.documents_files = [
383
- [
384
- { id: Refile.cache.upload(Refile::FileDouble.new("moo")).id }
385
- ].to_json,
386
- Refile::FileDouble.new("hello"),
387
- Refile::FileDouble.new("world")
388
- ]
389
- expect(post.documents[0].file.read).to eq("hello")
390
- expect(post.documents[1].file.read).to eq("world")
391
- expect(post.documents.size).to eq(2)
392
- end
393
-
394
- it "clears previously assigned files" do
395
- post.documents_files = [
396
- Refile::FileDouble.new("hello"),
397
- Refile::FileDouble.new("world")
398
- ]
399
- post.save
400
- post.update_attributes documents_files: [
401
- Refile::FileDouble.new("foo")
402
- ]
403
- retrieved = post_class.find(post.id)
404
- expect(retrieved.documents[0].file.read).to eq("foo")
405
- expect(retrieved.documents.size).to eq(1)
406
- end
407
-
408
- context "with append: true" do
409
- let(:options) { { append: true } }
410
-
411
- it "appends to previously assigned files" do
412
- post.documents_files = [
413
- Refile::FileDouble.new("hello"),
414
- Refile::FileDouble.new("world")
415
- ]
416
- post.save
417
- post.update_attributes documents_files: [
418
- Refile::FileDouble.new("foo")
419
- ]
420
- retrieved = post_class.find(post.id)
421
- expect(retrieved.documents[0].file.read).to eq("hello")
422
- expect(retrieved.documents[1].file.read).to eq("world")
423
- expect(retrieved.documents[2].file.read).to eq("foo")
424
- expect(retrieved.documents.size).to eq(3)
425
- end
426
-
427
- it "appends to previously assigned files with cached files" do
428
- post.documents_files = [
429
- Refile::FileDouble.new("hello"),
430
- Refile::FileDouble.new("world")
431
- ]
432
- post.save
433
- post.update_attributes documents_files: [
434
- [{
435
- id: Refile.cache.upload(Refile::FileDouble.new("hello")).id,
436
- filename: "some.jpg",
437
- content_type: "image/jpeg",
438
- size: 1234
439
- }].to_json
440
- ]
441
- retrieved = post_class.find(post.id)
442
- expect(retrieved.documents.size).to eq(3)
443
- end
444
- end
445
- end
446
-
447
- describe "#:association_:name_data" do
448
- it "returns metadata of all files" do
449
- post.documents_files = [nil, Refile::FileDouble.new("hello"), Refile::FileDouble.new("world")]
450
- data = post.documents_files_data
451
- expect(Refile.cache.read(data[0][:id])).to eq("hello")
452
- expect(Refile.cache.read(data[1][:id])).to eq("world")
453
- expect(data.size).to eq(2)
454
- end
455
380
  end
456
381
  end
457
382
 
@@ -39,13 +39,36 @@ describe Refile::AttachmentHelper do
39
39
  end
40
40
 
41
41
  describe "#attachment_field" do
42
+ subject(:field) { attachment_field("post", :document, field_options) }
43
+ let(:field_options) { { object: klass.new } }
44
+ let(:html) { Capybara.string(field) }
45
+ let(:expected_field_name) { "post[0][document]" }
46
+ let(:selector_css) { "input[name='#{expected_field_name}'][type=hidden]" }
47
+ let(:input_css) { "input[name='post[document]'][type=hidden]" }
48
+
42
49
  context "with index given" do
43
- let(:html) { Capybara.string(attachment_field("post", :document, object: klass.new, index: 0)) }
50
+ let(:field_options) { super().merge index: 0 }
44
51
 
45
52
  it "generates file and hidden inputs with identical names" do
46
- field_name = "post[0][document]"
47
- expect(html).to have_field(field_name, type: "file")
48
- expect(html).to have_selector(:css, "input[name='#{field_name}'][type=hidden]", visible: false, count: 1)
53
+ expect(html).to have_field(expected_field_name, type: "file")
54
+ expect(html).to have_selector(:css, selector_css, visible: false, count: 1)
55
+ end
56
+ end
57
+
58
+ context "when attacher value is blank" do
59
+ let(:field_options) { super().merge object: klass.new(document: nil) }
60
+ it "generates metadata hidden with disabled attribute" do
61
+ expect(html.find(input_css, visible: false)["disabled"]).to eq "disabled"
62
+ end
63
+ end
64
+
65
+ context "when attacher value is present" do
66
+ let(:field_options) do
67
+ super().merge object: klass.new(document: StringIO.new("New params"))
68
+ end
69
+
70
+ it "generates metadata input without disabled attribute" do
71
+ expect(html.find(input_css, visible: false)["disabled"]).to be_nil
49
72
  end
50
73
  end
51
74
  end
@@ -1,3 +1,6 @@
1
+ require "refile/active_record_helper"
2
+ require_relative "support/accepts_attachments_for_shared_examples"
3
+
1
4
  describe Refile::Attachment do
2
5
  let(:options) { {} }
3
6
  let(:klass) do
@@ -189,7 +192,7 @@ describe Refile::Attachment do
189
192
  it "handles redirect loops by trowing errors" do
190
193
  expect do
191
194
  instance.remote_document_url = "http://www.example.com/loop"
192
- end.to raise_error(RestClient::Exception)
195
+ end.to raise_error(Refile::TooManyRedirects)
193
196
  end
194
197
  end
195
198
 
@@ -586,4 +589,55 @@ describe Refile::Attachment do
586
589
  expect { p klass.ancestors }
587
590
  .to output(/Refile::Attachment\(document\)/).to_stdout
588
591
  end
592
+
593
+ describe ".accepts_nested_attributes_for" do
594
+ it_should_behave_like "accepts_attachments_for" do
595
+ let(:options) { {} }
596
+
597
+ # This class is a PORO, but it's implementing an interface that's similar
598
+ # to ActiveRecord's in order to simplify specs via shared examples.
599
+ let(:post_class) do
600
+ opts = options
601
+ foo = document_class
602
+
603
+ Class.new do
604
+ extend Refile::Attachment
605
+
606
+ attr_accessor :documents
607
+
608
+ accepts_attachments_for(
609
+ :documents,
610
+ accessor_prefix: "documents_files",
611
+ collection_class: foo,
612
+ **opts
613
+ )
614
+
615
+ def initialize(attributes)
616
+ @documents = attributes[:documents]
617
+ end
618
+
619
+ def save!; end
620
+
621
+ def update_attributes!(attributes)
622
+ attributes.each { |k, v| public_send("#{k}=", v) }
623
+ end
624
+ end
625
+ end
626
+
627
+ let(:document_class) do
628
+ Class.new do
629
+ extend Refile::Attachment
630
+ attr_accessor :file_id
631
+
632
+ attachment :file, type: :image, extension: %w[jpeg], raise_errors: false
633
+
634
+ def initialize(attributes = {})
635
+ self.file = attributes[:file]
636
+ end
637
+ end
638
+ end
639
+
640
+ let(:post) { post_class.new(documents: []) }
641
+ end
642
+ end
589
643
  end
@@ -0,0 +1,51 @@
1
+ describe Refile::Download do
2
+ context "without redirects" do
3
+ it "fetches the file" do
4
+ stub_request(:get, "http://www.example.com/dummy").to_return(
5
+ status: 200,
6
+ body: "dummy",
7
+ headers: { "Content-Length" => 5, "Content-Type" => "text/plain" }
8
+ )
9
+
10
+ download = described_class.new("http://www.example.com/dummy")
11
+
12
+ expect(download.io.read).to eq("dummy")
13
+ expect(download.size).to eq(5)
14
+ expect(download.content_type).to eq("text/plain")
15
+ expect(download.original_filename).to eq("dummy")
16
+ end
17
+ end
18
+
19
+ context "with redirects" do
20
+ it "follows redirects and fetches the file" do
21
+ stub_request(:get, "http://www.example.com/1").to_return(
22
+ status: 302,
23
+ headers: { "Location" => "http://www.example.com/2" }
24
+ )
25
+
26
+ stub_request(:get, "http://www.example.com/2").to_return(
27
+ status: 200,
28
+ body: "dummy",
29
+ headers: { "Content-Length" => 5 }
30
+ )
31
+
32
+ download = described_class.new("http://www.example.com/1")
33
+
34
+ expect(download.io.read).to eq("dummy")
35
+ expect(download.size).to eq(5)
36
+ expect(download.content_type).to eq("application/octet-stream")
37
+ expect(download.original_filename).to eq("2")
38
+ end
39
+
40
+ it "handles redirect loops by throwing errors" do
41
+ stub_request(:get, "http://www.example.com/loop").to_return(
42
+ status: 302,
43
+ headers: { "Location" => "http://www.example.com/loop" }
44
+ )
45
+
46
+ expect do
47
+ described_class.new("http://www.example.com/loop")
48
+ end.to raise_error(Refile::TooManyRedirects)
49
+ end
50
+ end
51
+ end
@@ -139,6 +139,6 @@ feature "Normal HTTP Post file uploads" do
139
139
  expect(download_link("Document")).to eq("abc")
140
140
  expect(page).to have_selector(".content-type", text: "image/png")
141
141
  expect(page).to have_selector(".size", text: "3")
142
- expect(page).to have_selector(".filename", text: "some_file.png", exact: true)
142
+ expect(page).to have_selector(".filename", text: "some_file.png")
143
143
  end
144
144
  end
@@ -0,0 +1,38 @@
1
+ require "refile/test_app"
2
+
3
+ feature "single attribute form upload" do
4
+ scenario "upload a single file insteaf of an array of files" do
5
+ visit "/single/posts/new"
6
+ fill_in "Title", with: "A cool post"
7
+ attach_file "Documents", path("hello.txt")
8
+ click_button "Create"
9
+
10
+ expect(download_link("Document: hello.txt")).to eq("hello")
11
+ end
12
+
13
+ scenario "Edit with changes" do
14
+ visit "/single/posts/new"
15
+ fill_in "Title", with: "A cool post"
16
+ attach_file "Documents", path("hello.txt")
17
+ click_button "Create"
18
+
19
+ visit "/single/posts/#{Post.last.id}/edit"
20
+ attach_file "Documents", path("monkey.txt")
21
+ click_button "Update"
22
+
23
+ expect(download_link("Document: monkey.txt")).to eq("monkey")
24
+ expect(page).not_to have_link("Document: hello.txt")
25
+ end
26
+
27
+ scenario "Edit without changes" do
28
+ visit "/single/posts/new"
29
+ fill_in "Title", with: "A cool post"
30
+ attach_file "Documents", path("hello.txt")
31
+ click_button "Create"
32
+
33
+ visit "/single/posts/#{Post.last.id}/edit"
34
+ click_button "Update"
35
+
36
+ expect(download_link("Document: hello.txt")).to eq("hello")
37
+ end
38
+ end
@@ -0,0 +1,232 @@
1
+ RSpec.shared_examples "accepts_attachments_for" do
2
+ describe "#:association_:name=" do
3
+ it "builds records from assigned files" do
4
+ post.documents_files = [
5
+ Refile::FileDouble.new("hello", content_type: "image/jpeg"),
6
+ Refile::FileDouble.new("world", content_type: "image/jpeg")
7
+ ]
8
+ post.save!
9
+
10
+ expect(post.documents.size).to eq(2)
11
+ expect(post.documents[0].file.read).to eq("hello")
12
+ expect(post.documents[1].file.read).to eq("world")
13
+ end
14
+
15
+ it "clears previously assigned files" do
16
+ post.documents_files = [
17
+ Refile::FileDouble.new("hello", content_type: "image/jpeg"),
18
+ Refile::FileDouble.new("world", content_type: "image/jpeg")
19
+ ]
20
+ post.save!
21
+ post.update_attributes! documents_files: [
22
+ Refile::FileDouble.new("foo", content_type: "image/jpeg")
23
+ ]
24
+
25
+ expect(post.documents[0].file.read).to eq("foo")
26
+ expect(post.documents.size).to eq(1)
27
+ end
28
+
29
+ it "ignores nil assigned files" do
30
+ post.documents_files = [
31
+ Refile::FileDouble.new("hello", content_type: "image/jpeg"),
32
+ nil,
33
+ nil
34
+ ]
35
+ post.save!
36
+
37
+ expect(post.documents.size).to eq(1)
38
+ expect(post.documents[0].file.read).to eq("hello")
39
+ end
40
+
41
+ it "builds records from cache" do
42
+ post.documents_files = [
43
+ [
44
+ {
45
+ id: Refile.cache.upload(Refile::FileDouble.new("hello")).id,
46
+ filename: "some.jpg",
47
+ content_type: "image/jpeg",
48
+ size: 1234
49
+ },
50
+ {
51
+ id: Refile.cache.upload(Refile::FileDouble.new("world")).id,
52
+ filename: "some.jpg",
53
+ content_type: "image/jpeg",
54
+ size: 1234
55
+ }
56
+ ].to_json
57
+ ]
58
+ post.save!
59
+
60
+ expect(post.documents.size).to eq(2)
61
+ expect(post.documents[0].file.read).to eq("hello")
62
+ expect(post.documents[1].file.read).to eq("world")
63
+ end
64
+
65
+ it "prefers uploaded files over cache when both are present" do
66
+ post.documents_files = [
67
+ [
68
+ {
69
+ id: Refile.cache.upload(Refile::FileDouble.new("moo")).id,
70
+ filename: "some.jpg",
71
+ content_type: "image/jpeg",
72
+ size: 1234
73
+ }
74
+ ].to_json,
75
+ Refile::FileDouble.new("hello", content_type: "image/jpeg"),
76
+ Refile::FileDouble.new("world", content_type: "image/jpeg")
77
+ ]
78
+ post.save!
79
+
80
+ expect(post.documents.size).to eq(2)
81
+ expect(post.documents[0].file.read).to eq("hello")
82
+ expect(post.documents[1].file.read).to eq("world")
83
+ end
84
+
85
+ it "ignores empty caches" do
86
+ post.documents_files = [
87
+ [
88
+ {
89
+ id: Refile.cache.upload(Refile::FileDouble.new("moo")).id,
90
+ filename: "some.jpg",
91
+ content_type: "image/jpeg",
92
+ size: 1234
93
+ },
94
+ {},
95
+ {}
96
+ ].to_json
97
+ ]
98
+ post.save!
99
+
100
+ expect(post.documents.size).to eq(1)
101
+ expect(post.documents[0].file.read).to eq("moo")
102
+ end
103
+
104
+ it "ignores caches with malformed json" do
105
+ post.documents_files = [
106
+ "[{id: 'this is a ruby hash'}]"
107
+ ]
108
+
109
+ expect(post.documents.size).to be_zero
110
+ end
111
+
112
+ context "with append: true" do
113
+ let(:options) { { append: true } }
114
+
115
+ it "appends to previously assigned files" do
116
+ post.documents_files = [
117
+ Refile::FileDouble.new("hello", content_type: "image/jpeg"),
118
+ Refile::FileDouble.new("world", content_type: "image/jpeg")
119
+ ]
120
+ post.save!
121
+ post.update_attributes! documents_files: [
122
+ Refile::FileDouble.new("foo", content_type: "image/jpeg")
123
+ ]
124
+
125
+ expect(post.documents.size).to eq(3)
126
+ expect(post.documents[0].file.read).to eq("hello")
127
+ expect(post.documents[1].file.read).to eq("world")
128
+ expect(post.documents[2].file.read).to eq("foo")
129
+ end
130
+
131
+ it "appends to previously assigned files with cached files" do
132
+ post.documents_files = [
133
+ Refile::FileDouble.new("hello", content_type: "image/jpeg"),
134
+ Refile::FileDouble.new("world", content_type: "image/jpeg")
135
+ ]
136
+ post.save!
137
+ post.update_attributes! documents_files: [
138
+ [{
139
+ id: Refile.cache.upload(Refile::FileDouble.new("hello world")).id,
140
+ filename: "some.jpg",
141
+ content_type: "image/jpeg",
142
+ size: 1234
143
+ }].to_json
144
+ ]
145
+
146
+ expect(post.documents.size).to eq(3)
147
+ expect(post.documents[0].file.read).to eq("hello")
148
+ expect(post.documents[1].file.read).to eq("world")
149
+ expect(post.documents[2].file.read).to eq("hello world")
150
+ end
151
+
152
+ it "appends to previously cached files with cached files" do
153
+ post.documents_files = [
154
+ [
155
+ {
156
+ id: Refile.cache.upload(Refile::FileDouble.new("moo")).id,
157
+ filename: "some1.jpg",
158
+ content_type: "image/jpeg",
159
+ size: 123
160
+ }
161
+ ].to_json
162
+ ]
163
+ post.documents_files = [
164
+ [
165
+ {
166
+ id: Refile.cache.upload(Refile::FileDouble.new("hello")).id,
167
+ filename: "some2.jpg",
168
+ content_type: "image/jpeg",
169
+ size: 1234
170
+ }
171
+ ].to_json
172
+ ]
173
+ post.save!
174
+
175
+ expect(post.documents.size).to eq(2)
176
+ expect(post.documents[0].file.read).to eq("moo")
177
+ expect(post.documents[1].file.read).to eq("hello")
178
+ end
179
+ end
180
+ end
181
+
182
+ describe "#:association_:name_data" do
183
+ it "returns metadata for all files" do
184
+ post.documents_files = [
185
+ nil,
186
+ Refile::FileDouble.new("hello", content_type: "image/jpeg"),
187
+ Refile::FileDouble.new("world", content_type: "image/jpeg")
188
+ ]
189
+ data = post.documents_files_data
190
+
191
+ expect(data.size).to eq(2)
192
+ expect(Refile.cache.read(data[0][:id])).to eq("hello")
193
+ expect(Refile.cache.read(data[1][:id])).to eq("world")
194
+ end
195
+
196
+ context "when there are invalid files" do
197
+ it "only returns metadata for valid files " do
198
+ invalid_file = Refile::FileDouble.new("world", content_type: "text/plain")
199
+
200
+ post.documents_files = [invalid_file]
201
+ data = post.documents_files_data
202
+
203
+ expect(data).to be_nil
204
+ end
205
+ end
206
+ end
207
+
208
+ describe "#:association_:name" do
209
+ it "builds records from assigned files" do
210
+ post.documents_files = [
211
+ Refile::FileDouble.new("hello", content_type: "image/jpeg"),
212
+ Refile::FileDouble.new("world", content_type: "image/jpeg")
213
+ ]
214
+
215
+ expect(post.documents_files.size).to eq(2)
216
+ expect(post.documents_files[0].read).to eq("hello")
217
+ expect(post.documents_files[1].read).to eq("world")
218
+ end
219
+ end
220
+
221
+ describe "#:association_:name_attachment_definition" do
222
+ it "returns attachment definition" do
223
+ post.documents_files = [
224
+ Refile::FileDouble.new("hello", content_type: "image/jpeg")
225
+ ]
226
+
227
+ definition = post.documents_files_attachment_definition
228
+ expect(definition).to be_a Refile::AttachmentDefinition
229
+ expect(definition.name).to eq(:file)
230
+ end
231
+ end
232
+ end
metadata CHANGED
@@ -1,35 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leifcr-refile
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonas Nicklas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-04-08 00:00:00.000000000 Z
11
+ date: 2019-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: rest-client
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '1.8'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '3.0'
23
- type: :runtime
24
- prerelease: false
25
- version_requirements: !ruby/object:Gem::Requirement
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: '1.8'
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: '3.0'
33
13
  - !ruby/object:Gem::Dependency
34
14
  name: sinatra
35
15
  requirement: !ruby/object:Gem::Requirement
@@ -79,11 +59,13 @@ files:
79
59
  - lib/refile/attacher.rb
80
60
  - lib/refile/attachment.rb
81
61
  - lib/refile/attachment/active_record.rb
62
+ - lib/refile/attachment/multiple_attachments.rb
82
63
  - lib/refile/attachment_definition.rb
83
64
  - lib/refile/backend/file_system.rb
84
65
  - lib/refile/backend/s3.rb
85
66
  - lib/refile/backend_macros.rb
86
67
  - lib/refile/custom_logger.rb
68
+ - lib/refile/download.rb
87
69
  - lib/refile/file.rb
88
70
  - lib/refile/file_double.rb
89
71
  - lib/refile/image_processing.rb
@@ -103,17 +85,20 @@ files:
103
85
  - spec/refile/backend_examples.rb
104
86
  - spec/refile/backend_macros_spec.rb
105
87
  - spec/refile/custom_logger_spec.rb
88
+ - spec/refile/download_spec.rb
106
89
  - spec/refile/features/direct_upload_spec.rb
107
90
  - spec/refile/features/multiple_upload_spec.rb
108
91
  - spec/refile/features/normal_upload_spec.rb
109
92
  - spec/refile/features/presigned_upload_spec.rb
110
93
  - spec/refile/features/simple_form_spec.rb
94
+ - spec/refile/features/single_upload_spec.rb
111
95
  - spec/refile/fixtures/hello.txt
112
96
  - spec/refile/fixtures/image.jpg
113
97
  - spec/refile/fixtures/large.txt
114
98
  - spec/refile/fixtures/monkey.txt
115
99
  - spec/refile/fixtures/world.txt
116
100
  - spec/refile/spec_helper.rb
101
+ - spec/refile/support/accepts_attachments_for_shared_examples.rb
117
102
  - spec/refile_spec.rb
118
103
  homepage: https://github.com/refile/refile
119
104
  licenses:
@@ -135,8 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
120
  - !ruby/object:Gem::Version
136
121
  version: '0'
137
122
  requirements: []
138
- rubyforge_project:
139
- rubygems_version: 2.6.14
123
+ rubygems_version: 3.0.3
140
124
  signing_key:
141
125
  specification_version: 4
142
126
  summary: Simple and powerful file upload library