leifcr-refile 0.6.3 → 0.7.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.
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