Skip to content

Commit 1adea93

Browse files
committed
[ActiveStorage] Add ability to use pre-defined variants
1 parent ea4acf5 commit 1adea93

File tree

8 files changed

+117
-8
lines changed

8 files changed

+117
-8
lines changed

activestorage/CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
* Add ability to use pre-defined variants.
2+
3+
```ruby
4+
class User < ActiveRecord::Base
5+
has_one_attached :avatar, variants: {
6+
thumb: { resize: "100x100" },
7+
medium: { resize: "300x300", monochrome: true }
8+
}
9+
end
10+
11+
class Gallery < ActiveRecord::Base
12+
has_many_attached :photos, variants: {
13+
thumb: { resize: "100x100" },
14+
medium: { resize: "300x300", monochrome: true }
15+
}
16+
end
17+
18+
<%= image_tag user.avatar.variant(:thumb) %>
19+
```
20+
21+
*fatkodima*
22+
123
* Add `config.active_storage.web_image_content_types` to allow applications
224
to add content types (like `image/webp`) in which variants can be processed,
325
instead of letting those images be converted to the fallback PNG format.

activestorage/app/models/active_storage/attachment.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ def purge_later
3030
blob&.purge_later
3131
end
3232

33+
def variant(transformations)
34+
case transformations
35+
when Symbol
36+
variant_name = transformations
37+
transformations = variants.fetch(variant_name) do
38+
record_model_name = record.to_model.model_name.name
39+
raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
40+
end
41+
end
42+
43+
blob.variant(transformations)
44+
end
45+
3346
private
3447
def identify_blob
3548
blob.identify
@@ -51,6 +64,10 @@ def purge_dependent_blob_later
5164
def dependent
5265
record.attachment_reflections[name]&.options[:dependent]
5366
end
67+
68+
def variants
69+
record.attachment_reflections[name]&.options[:variants]
70+
end
5471
end
5572

5673
ActiveSupport.run_load_hooks :active_storage_attachment, ActiveStorage::Attachment

activestorage/lib/active_storage/attached/model.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ module Attached::Model
4040
# has_one_attached :avatar, service: :s3
4141
# end
4242
#
43-
def has_one_attached(name, dependent: :purge_later, service: nil)
43+
def has_one_attached(name, dependent: :purge_later, service: nil, variants: {})
4444
validate_service_configuration(name, service)
4545

4646
generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -72,7 +72,7 @@ def #{name}=(attachable)
7272
:has_one_attached,
7373
name,
7474
nil,
75-
{ dependent: dependent, service_name: service },
75+
{ dependent: dependent, service_name: service, variants: variants },
7676
self
7777
)
7878
ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection)
@@ -110,7 +110,7 @@ def #{name}=(attachable)
110110
# has_many_attached :photos, service: :s3
111111
# end
112112
#
113-
def has_many_attached(name, dependent: :purge_later, service: nil)
113+
def has_many_attached(name, dependent: :purge_later, service: nil, variants: {})
114114
validate_service_configuration(name, service)
115115

116116
generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -159,7 +159,7 @@ def purge_later
159159
:has_many_attached,
160160
name,
161161
nil,
162-
{ dependent: dependent, service_name: service },
162+
{ dependent: dependent, service_name: service, variants: variants },
163163
self
164164
)
165165
ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection)

activestorage/test/models/attached/many_test.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,26 @@ def highlights
610610
assert_match(/Cannot configure service :unknown for User#featured_photos/, error.message)
611611
end
612612

613+
test "creating variation by variation name" do
614+
@user.highlights_with_variants.attach fixture_file_upload("racecar.jpg")
615+
variant = @user.highlights_with_variants.first.variant(:thumb).processed
616+
617+
image = read_image(variant)
618+
assert_equal "JPEG", image.type
619+
assert_equal 100, image.width
620+
assert_equal 67, image.height
621+
end
622+
623+
test "raises error when unknown variant name is used" do
624+
@user.highlights_with_variants.attach fixture_file_upload("racecar.jpg")
625+
626+
error = assert_raises ArgumentError do
627+
@user.highlights_with_variants.first.variant(:unknown).processed
628+
end
629+
630+
assert_match(/Cannot find variant :unknown for User#highlights_with_variants/, error.message)
631+
end
632+
613633
private
614634
def append_on_assign
615635
ActiveStorage.replace_on_assign_to_many, previous = false, ActiveStorage.replace_on_assign_to_many

activestorage/test/models/attached/one_test.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,4 +584,24 @@ def avatar
584584

585585
assert_match(/Cannot configure service :unknown for User#featured_photo/, error.message)
586586
end
587+
588+
test "creating variation by variation name" do
589+
@user.avatar_with_variants.attach fixture_file_upload("racecar.jpg")
590+
variant = @user.avatar_with_variants.variant(:thumb).processed
591+
592+
image = read_image(variant)
593+
assert_equal "JPEG", image.type
594+
assert_equal 100, image.width
595+
assert_equal 67, image.height
596+
end
597+
598+
test "raises error when unknown variant name is used" do
599+
@user.avatar_with_variants.attach fixture_file_upload("racecar.jpg")
600+
601+
error = assert_raises ArgumentError do
602+
@user.avatar_with_variants.variant(:unknown).processed
603+
end
604+
605+
assert_match(/Cannot find variant :unknown for User#avatar_with_variants/, error.message)
606+
end
587607
end

activestorage/test/models/reflection_test.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class ActiveStorage::ReflectionTest < ActiveSupport::TestCase
1212

1313
reflection = User.reflect_on_attachment(:cover_photo)
1414
assert_equal :local, reflection.options[:service_name]
15+
16+
reflection = User.reflect_on_attachment(:avatar_with_variants)
17+
assert_instance_of Hash, reflection.options[:variants]
1518
end
1619

1720
test "reflection on a singular attachment with the same name as an attachment on another model" do
@@ -28,13 +31,16 @@ class ActiveStorage::ReflectionTest < ActiveSupport::TestCase
2831

2932
reflection = User.reflect_on_attachment(:vlogs)
3033
assert_equal :local, reflection.options[:service_name]
34+
35+
reflection = User.reflect_on_attachment(:highlights_with_variants)
36+
assert_instance_of Hash, reflection.options[:variants]
3137
end
3238

3339
test "reflecting on all attachments" do
3440
reflections = User.reflect_on_all_attachments.sort_by(&:name)
3541
assert_equal [ User ], reflections.collect(&:active_record).uniq
36-
assert_equal %i[ avatar cover_photo highlights vlogs ], reflections.collect(&:name)
37-
assert_equal %i[ has_one_attached has_one_attached has_many_attached has_many_attached ], reflections.collect(&:macro)
38-
assert_equal [ :purge_later, false, :purge_later, false ], reflections.collect { |reflection| reflection.options[:dependent] }
42+
assert_equal %i[ avatar avatar_with_variants cover_photo highlights highlights_with_variants vlogs ], reflections.collect(&:name)
43+
assert_equal %i[ has_one_attached has_one_attached has_one_attached has_many_attached has_many_attached has_many_attached ], reflections.collect(&:macro)
44+
assert_equal [ :purge_later, :purge_later, false, :purge_later, :purge_later, false ], reflections.collect { |reflection| reflection.options[:dependent] }
3945
end
4046
end

activestorage/test/test_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,11 @@ class User < ActiveRecord::Base
119119

120120
has_one_attached :avatar
121121
has_one_attached :cover_photo, dependent: false, service: :local
122+
has_one_attached :avatar_with_variants, variants: { thumb: { resize: "100x100" } }
122123

123124
has_many_attached :highlights
124125
has_many_attached :vlogs, dependent: false, service: :local
126+
has_many_attached :highlights_with_variants, variants: { thumb: { resize: "100x100" } }
125127
end
126128

127129
class Group < ActiveRecord::Base

guides/source/active_storage_overview.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ amazon:
129129
secret_access_key: ""
130130
region: ""
131131
bucket: ""
132-
upload:
132+
upload:
133133
server_side_encryption: "" # 'aws:kms' or 'AES256'
134134
```
135135

@@ -332,6 +332,20 @@ class User < ApplicationRecord
332332
end
333333
```
334334

335+
You can configure specific variants per attachment using the `variants` option:
336+
337+
```ruby
338+
class User < ApplicationRecord
339+
has_one_attached :avatar, variants: { thumb: { resize: "100x100" } }
340+
end
341+
```
342+
343+
Call `avatar.variant(:thumb)` to get a thumb variant of an avatar:
344+
345+
```ruby
346+
<%= image_tag user.avatar.variant(:thumb) %>
347+
```
348+
335349
### `has_many_attached`
336350

337351
The `has_many_attached` macro sets up a one-to-many relationship between records
@@ -382,6 +396,14 @@ class Message < ApplicationRecord
382396
end
383397
```
384398

399+
Configuring specific variants is done the same way as `has_one_attached`, by using the `variants` option:
400+
401+
```ruby
402+
class Message < ApplicationRecord
403+
has_many_attached :images, variants: { thumb: { resize: "100x100" } }
404+
end
405+
```
406+
385407
### Attaching File/IO Objects
386408

387409
Sometimes you need to attach a file that doesn’t arrive via an HTTP request.

0 commit comments

Comments
 (0)