decidim-core 0.29.0.rc3 → 0.29.0.rc4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/card_g/show.erb +1 -1
  3. data/app/cells/decidim/card_g_cell.rb +5 -2
  4. data/app/cells/decidim/card_l/image.erb +2 -2
  5. data/app/cells/decidim/card_l_cell.rb +5 -2
  6. data/app/cells/decidim/content_blocks/hero_cell.rb +1 -1
  7. data/app/cells/decidim/content_blocks/highlighted_content_banner/show.erb +1 -1
  8. data/app/cells/decidim/content_blocks/participatory_space_hero_cell.rb +2 -2
  9. data/app/commands/decidim/create_omniauth_registration.rb +10 -4
  10. data/app/controllers/concerns/decidim/devise_controllers.rb +1 -0
  11. data/app/controllers/decidim/application_controller.rb +1 -0
  12. data/app/models/decidim/attachment.rb +8 -7
  13. data/app/models/decidim/content_block.rb +2 -2
  14. data/app/models/decidim/user.rb +12 -12
  15. data/app/packs/stylesheets/decidim/_filters.scss +3 -1
  16. data/app/packs/stylesheets/decidim/_footer.scss +1 -1
  17. data/app/views/decidim/manifests/show.json.erb +4 -4
  18. data/app/views/layouts/decidim/_logo.html.erb +1 -1
  19. data/app/views/layouts/decidim/footer/_main.html.erb +1 -1
  20. data/app/views/layouts/decidim/footer/_main_intro.html.erb +1 -1
  21. data/app/views/layouts/decidim/header/_main_links_desktop.html.erb +1 -1
  22. data/app/views/layouts/decidim/header/_main_links_mobile_account.html.erb +1 -1
  23. data/app/views/layouts/decidim/header/_main_links_mobile_item_account.html.erb +1 -1
  24. data/lib/decidim/asset_router/storage.rb +216 -13
  25. data/lib/decidim/core/test/shared_examples/attachable_interface_examples.rb +1 -1
  26. data/lib/decidim/core/version.rb +1 -1
  27. metadata +6 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d58ac45e047a62b04a7625b6da1c4ee92c2a62d9bf4e1969cdfb324257e5ef9
4
- data.tar.gz: 94a06bf8519d6234945a39dc86cc9794149fa3dc09c89273dcd475a64a3ad78b
3
+ metadata.gz: 3394fbf408e9109f688cd6e8fa7439096050219d59f5ac6328f6be21a51c752d
4
+ data.tar.gz: b39954abe8812022859ab60690a82dc89078868cfe89b567ea1a2499d7cb7aa0
5
5
  SHA512:
6
- metadata.gz: 6e90186e86e894822888b13cbaabe6c8c2a752d73a477cb13b39be5fd785d9b92082e5e403de91790f43c19a78f83bb85253e4fa0475f086f64e90258b2593b3
7
- data.tar.gz: f297c4b6a5fad64c57984fc063778cddda2404f3861e75b8a16c4fff83986caedc44354bba11a58551c5815dae0c9785b5f8d159a9a7e0930d2f080341b69579
6
+ metadata.gz: 9402884a433f7bd1d3498bc70429be6704f56c2e3eb4fdb4769ea076d1c9473ee555b5580d142cb7560b6a1ea54d027415ec9821303466fb56c8bfa1889ea611
7
+ data.tar.gz: 4fcd613c47e4d1a8eed16ef0f3217afa43b4a648f49dbe6a787cb82e7919ef2ba290b1c07b2e39da7d91098620046221b2552eaddc091982eacc03e1f4141db1
@@ -1,7 +1,7 @@
1
1
  <%= link_to resource_path, class: classes[:default], id: resource_id do %>
2
2
  <div class="<%= classes[:img] %>">
3
3
  <% if has_image? %>
4
- <%= image_tag resource_image_path, alt: alt_title %>
4
+ <%= image_tag resource_image_url, alt: alt_title %>
5
5
  <% else %>
6
6
  <%= external_icon "media/images/placeholder-card-g.svg", class: "card__placeholder-g" %>
7
7
  <% end %>
@@ -52,12 +52,15 @@ module Decidim
52
52
  @id_base_name ||= resource.class.name.gsub(/\ADecidim::/, "").underscore.split("/").join("__")
53
53
  end
54
54
 
55
- def resource_image_path
55
+ def resource_image_url
56
+ # Backwards compatibility.
57
+ return resource_image_path if respond_to?(:resource_image_path)
58
+
56
59
  nil
57
60
  end
58
61
 
59
62
  def has_image?
60
- resource_image_path.present?
63
+ resource_image_url.present?
61
64
  end
62
65
 
63
66
  def show_description?
@@ -1,6 +1,6 @@
1
1
  <div class="card__list-image">
2
- <% if resource_image_path.present? %>
3
- <%= image_tag resource_image_path, class: "w-full h-full object-cover" %>
2
+ <% if has_image? %>
3
+ <%= image_tag resource_image_url, class: "w-full h-full object-cover" %>
4
4
  <% else %>
5
5
  <div class="w-full h-full relative">
6
6
  <div class="w-full h-full bg-primary opacity-10 absolute top-0 left-0 z-10">
@@ -66,12 +66,15 @@ module Decidim
66
66
  "#{class_base_name}__#{class_name}"
67
67
  end
68
68
 
69
- def resource_image_path
69
+ def resource_image_url
70
+ # Backwards compatibility.
71
+ return resource_image_path if respond_to?(:resource_image_path)
72
+
70
73
  nil
71
74
  end
72
75
 
73
76
  def has_image?
74
- resource_image_path.present?
77
+ resource_image_url.present?
75
78
  end
76
79
 
77
80
  def has_link_to_resource?
@@ -15,7 +15,7 @@ module Decidim
15
15
  end
16
16
 
17
17
  def background_image
18
- model.images_container.attached_uploader(:background_image).path(variant: :big)
18
+ model.images_container.attached_uploader(:background_image).variant_url(:big)
19
19
  end
20
20
 
21
21
  private
@@ -1,4 +1,4 @@
1
- <section id="highlighted_content_banner" class="home__section-image" style="--hero-image:url('<%= current_organization.attached_uploader(:highlighted_content_banner_image).path %>');">
1
+ <section id="highlighted_content_banner" class="home__section-image" style="--hero-image:url('<%= current_organization.attached_uploader(:highlighted_content_banner_image).url %>');">
2
2
  <div class="home__section-content-banner home__section">
3
3
  <div>
4
4
  <h2 class="home__section-content-banner__title">
@@ -32,9 +32,9 @@ module Decidim
32
32
  # If it is called from the landing page content block, use the background image defined there
33
33
  # Else, use the banner image defined in the space (for assemblies)
34
34
  def image_path
35
- return model.images_container.attached_uploader(:background_image).path if model.respond_to?(:images_container)
35
+ return model.images_container.attached_uploader(:background_image).url if model.respond_to?(:images_container)
36
36
 
37
- attached_uploader(:banner_image).path
37
+ attached_uploader(:banner_image).url
38
38
  end
39
39
 
40
40
  def has_hashtag?
@@ -55,7 +55,12 @@ module Decidim
55
55
  # If user has left the account unconfirmed and later on decides to sign
56
56
  # in with omniauth with an already verified account, the account needs
57
57
  # to be marked confirmed.
58
- @user.skip_confirmation! if !@user.confirmed? && @user.email == verified_email
58
+ if !@user.confirmed? && @user.email == verified_email
59
+ @user.skip_confirmation!
60
+ @user.after_confirmation
61
+ end
62
+ @user.tos_agreement = "1"
63
+ @user.save!
59
64
  else
60
65
  @user.email = (verified_email || form.email)
61
66
  @user.name = form.name
@@ -69,10 +74,11 @@ module Decidim
69
74
  @user.avatar.attach(io: file, filename:)
70
75
  end
71
76
  @user.skip_confirmation! if verified_email
72
- end
77
+ @user.tos_agreement = "1"
78
+ @user.save!
73
79
 
74
- @user.tos_agreement = "1"
75
- @user.save!
80
+ @user.after_confirmation if verified_email
81
+ end
76
82
  end
77
83
 
78
84
  def create_identity
@@ -21,6 +21,7 @@ module Decidim
21
21
  include Decidim::SafeRedirect
22
22
  include NeedsSnippets
23
23
  include UserBlockedChecker
24
+ include ActiveStorage::SetCurrent
24
25
 
25
26
  helper Decidim::TranslationsHelper
26
27
  helper Decidim::MetaTagsHelper
@@ -24,6 +24,7 @@ module Decidim
24
24
  include DisableRedirectionToExternalHost
25
25
  include NeedsPasswordChange
26
26
  include LinkedResourceReference
27
+ include ActiveStorage::SetCurrent
27
28
 
28
29
  helper Decidim::MetaTagsHelper
29
30
  helper Decidim::DecidimFormHelper
@@ -84,11 +84,12 @@ module Decidim
84
84
  #
85
85
  # Returns String.
86
86
  def url
87
- if file?
88
- attached_uploader(:file).path
89
- elsif link?
90
- link
91
- end
87
+ @url ||=
88
+ if file?
89
+ attached_uploader(:file).url
90
+ elsif link?
91
+ link
92
+ end
92
93
  end
93
94
 
94
95
  # The URL to download the thumbnail of the file. Only works with images.
@@ -97,7 +98,7 @@ module Decidim
97
98
  def thumbnail_url
98
99
  return unless photo?
99
100
 
100
- attached_uploader(:file).path(variant: :thumbnail)
101
+ @thumbnail_url ||= attached_uploader(:file).variant_url(:thumbnail)
101
102
  end
102
103
 
103
104
  # The URL to download the a big version of the file. Only works with images.
@@ -106,7 +107,7 @@ module Decidim
106
107
  def big_url
107
108
  return unless photo?
108
109
 
109
- attached_uploader(:file).path(variant: :big)
110
+ @big_url ||= attached_uploader(:file).variant_url(:big)
110
111
  end
111
112
 
112
113
  def set_content_type_and_size
@@ -60,8 +60,8 @@ module Decidim
60
60
  #
61
61
  # # This is how you can access the image data, just like with any other
62
62
  # # uploader field. You can use the uploader variants too.
63
- # content_block.images_container.attached_uploader(:my_image).path
64
- # content_block.images_container.attached_uploader(:my_image).path(variant: :big)
63
+ # content_block.images_container.attached_uploader(:my_image).url
64
+ # content_block.images_container.attached_uploader(:my_image).variant_url(:big)
65
65
  #
66
66
  # # This will delete the attached image
67
67
  # content_block.images_container.my_image = nil
@@ -278,6 +278,18 @@ module Decidim
278
278
  false
279
279
  end
280
280
 
281
+ def after_confirmation
282
+ return unless organization.send_welcome_notification?
283
+
284
+ Decidim::EventsManager.publish(
285
+ event: "decidim.events.core.welcome_notification",
286
+ event_class: WelcomeNotificationEvent,
287
+ resource: self,
288
+ affected_users: [self],
289
+ extra: { force_email: true }
290
+ )
291
+ end
292
+
281
293
  protected
282
294
 
283
295
  # Overrides devise email required validation.
@@ -296,18 +308,6 @@ module Decidim
296
308
  super
297
309
  end
298
310
 
299
- def after_confirmation
300
- return unless organization.send_welcome_notification?
301
-
302
- Decidim::EventsManager.publish(
303
- event: "decidim.events.core.welcome_notification",
304
- event_class: WelcomeNotificationEvent,
305
- resource: self,
306
- affected_users: [self],
307
- extra: { force_email: true }
308
- )
309
- end
310
-
311
311
  private
312
312
 
313
313
  # Changes default Devise behaviour to use ActiveJob to send async emails.
@@ -1,6 +1,8 @@
1
1
  .filter {
2
+ @apply flex gap-2 p-1.5 relative;
3
+
2
4
  label {
3
- @apply flex items-center gap-2 p-1.5 rounded cursor-pointer relative;
5
+ @apply flex items-center gap-2 p-1.5 rounded cursor-pointer relative w-full;
4
6
  }
5
7
 
6
8
  &-container {
@@ -3,7 +3,7 @@ footer {
3
3
  @apply bg-gray-4;
4
4
 
5
5
  &__top {
6
- @apply hidden flex flex-col lg:flex-row lg:block gap-8 container py-10;
6
+ @apply hidden lg:flex flex-row gap-8 container py-10;
7
7
  }
8
8
 
9
9
  &__down {
@@ -8,22 +8,22 @@
8
8
  "background_color": "#FFFFFF",
9
9
  "icons": [
10
10
  {
11
- "src": "<%= current_organization.attached_uploader(:favicon).variant_path :small %>",
11
+ "src": "<%= current_organization.attached_uploader(:favicon).variant_url :small %>",
12
12
  "sizes": "32x32",
13
13
  "type": "image/png"
14
14
  },
15
15
  {
16
- "src": "<%= current_organization.attached_uploader(:favicon).variant_path :medium %>",
16
+ "src": "<%= current_organization.attached_uploader(:favicon).variant_url :medium %>",
17
17
  "sizes": "180x180",
18
18
  "type": "image/png"
19
19
  },
20
20
  {
21
- "src": "<%= current_organization.attached_uploader(:favicon).variant_path :big %>",
21
+ "src": "<%= current_organization.attached_uploader(:favicon).variant_url :big %>",
22
22
  "sizes": "192x192",
23
23
  "type": "image/png"
24
24
  },
25
25
  {
26
- "src": "<%= current_organization.attached_uploader(:favicon).variant_path :huge %>",
26
+ "src": "<%= current_organization.attached_uploader(:favicon).variant_url :huge %>",
27
27
  "sizes": "512x512",
28
28
  "type": "image/png"
29
29
  }
@@ -1,7 +1,7 @@
1
1
  <% if organization %>
2
2
  <%= link_to decidim.root_url(host: organization.host), "aria-label": t("front_page_link", scope: "decidim.accessibility") do %>
3
3
  <% if organization.logo.attached? %>
4
- <%= image_tag organization.attached_uploader(:logo).variant_path(:medium), alt: t("logo", scope: "decidim.accessibility", organization: current_organization_name) %>
4
+ <%= image_tag organization.attached_uploader(:logo).variant_url(:medium), alt: t("logo", scope: "decidim.accessibility", organization: current_organization_name) %>
5
5
  <% else %>
6
6
  <span><%= current_organization_name %></span>
7
7
  <% end %>
@@ -3,7 +3,7 @@
3
3
  <div class="lg:w-1/3">
4
4
  <%= render partial: "layouts/decidim/footer/main_intro" %>
5
5
  </div>
6
- <div class="lg:w-2/3 grid grid-cols-1 gap-x-44 gap-y-10 lg:grid-cols-4 text-md text-white">
6
+ <div class="lg:w-2/3 grid grid-cols-1 gap-x-4 gap-y-10 lg:grid-cols-4 text-md text-white">
7
7
  <%= render partial: "layouts/decidim/footer/main_links" %>
8
8
  </div>
9
9
  </div>
@@ -1,6 +1,6 @@
1
1
  <% if current_organization.official_img_footer.attached? %>
2
2
  <%= link_to current_organization.official_url, class: "block mb-6" do %>
3
- <%= image_tag current_organization.attached_uploader(:official_img_footer).path, alt: current_organization_name, class: "max-h-16" %>
3
+ <%= image_tag current_organization.attached_uploader(:official_img_footer).url, alt: current_organization_name, class: "max-h-16" %>
4
4
  <% end %>
5
5
  <% end %>
6
6
  <div class="text-sm text-white prose">
@@ -35,7 +35,7 @@
35
35
  <span class="main-bar__avatar">
36
36
  <span>
37
37
  <%= image_tag(
38
- current_user.attached_uploader(:avatar).path(variant: :thumb),
38
+ current_user.attached_uploader(:avatar).variant_url(:thumb),
39
39
  alt: t("decidim.author.avatar", name: decidim_sanitize(current_user.avatar.name))
40
40
  ) %>
41
41
  </span>
@@ -20,7 +20,7 @@
20
20
  <% if current_user.avatar.attached? %>
21
21
  <div class="main-bar__avatar">
22
22
  <%= image_tag(
23
- current_user.attached_uploader(:avatar).path(variant: :thumb),
23
+ current_user.attached_uploader(:avatar).variant_url(:thumb),
24
24
  alt: t("decidim.author.avatar", name: decidim_sanitize(current_user.avatar.name))
25
25
  ) %>
26
26
  </div>
@@ -6,7 +6,7 @@
6
6
  <% if current_user.avatar.attached? %>
7
7
  <span class="main-bar__avatar">
8
8
  <%= image_tag(
9
- current_user.attached_uploader(:avatar).path(variant: :thumb),
9
+ current_user.attached_uploader(:avatar).variant_url(:thumb),
10
10
  alt: t("decidim.author.avatar", name: decidim_sanitize(current_user.avatar.name))
11
11
  ) %>
12
12
  </span>
@@ -6,6 +6,23 @@ module Decidim
6
6
  # saved through ActiveStorage. This handles the different cases for routing
7
7
  # to the remote routes when using an assets CDN or to local routes when
8
8
  # using the local disk storage driver.
9
+ #
10
+ # Note that when the assets are stored in a remote storage service, such as
11
+ # Amazon S3, Google Cloud Storage or Azure Storage, this generates the asset
12
+ # URL directly to the storage service itself bypassing the Rails server and
13
+ # saving CPU time from serving the asset redirect requests. This causes a
14
+ # significant performance improvement on pages that display a lot of images.
15
+ # It will also produce a less significant performance improvement when using
16
+ # the local disk storage because in this situation, the images are served
17
+ # using one request instead of two when served directly from the storage
18
+ # service rather than through the asset redirect URL.
19
+ #
20
+ # When implementing changes to the logic, please keep the remote storage
21
+ # options and performance implications in mind because the specs for this
22
+ # utility do not cover the remote storage options because the extra
23
+ # configuration needed to test, the service itself needed for testing and
24
+ # the extra dependency overhead for adding these remote storage gems when
25
+ # they are not needed.
9
26
  class Storage
10
27
  # Initializes the router.
11
28
  #
@@ -13,25 +30,36 @@ module Decidim
13
30
  # to
14
31
  def initialize(asset)
15
32
  @asset = asset
33
+ @blob =
34
+ case asset
35
+ when ActiveStorage::Blob
36
+ asset
37
+ else
38
+ asset&.blob
39
+ end
16
40
  end
17
41
 
18
42
  # Generates the correct URL to the asset with the provided options.
19
43
  #
20
44
  # @param options The options for the URL that are the normal route options
21
45
  # Rails route helpers accept
22
- def url(**options)
23
- if asset.is_a? ActiveStorage::Attached
24
- routes.rails_blob_url(asset.blob, **default_options.merge(options))
25
- elsif asset.is_a? ActiveStorage::Blob
26
- routes.rails_blob_url(asset, **default_options.merge(options))
27
- else
28
- representation_url(**options)
46
+ # @return [String] The URL of the asset
47
+ def url(**)
48
+ case asset
49
+ when ActiveStorage::Attached
50
+ ensure_current_host(asset.record, **)
51
+ blob_url(**)
52
+ when ActiveStorage::Blob
53
+ blob_url(**)
54
+ else # ActiveStorage::VariantWithRecord, ActiveStorage::Variant
55
+ ensure_current_host(nil, **)
56
+ representation_url(**)
29
57
  end
30
58
  end
31
59
 
32
60
  private
33
61
 
34
- attr_reader :asset
62
+ attr_reader :asset, :blob
35
63
 
36
64
  # Provides the route helpers depending on whether the URL is generated to
37
65
  # the local host or an external CDN (remote).
@@ -80,24 +108,199 @@ module Decidim
80
108
  }.compact
81
109
  end
82
110
 
83
- # Converts the variation URLs last part to the correct file extension in
84
- # case the variation has a different format than the original image.
111
+ # Most of the times the current host should be set through the controller
112
+ # already when the logic below is unnecessary. This logic is needed e.g.
113
+ # for serializers where the request context is not available.
114
+ #
115
+ # @param record The record for which to check the organization
116
+ # @param opts Options for building the URL
117
+ # @return [void]
118
+ def ensure_current_host(record, **opts)
119
+ return if asset_url_available?
120
+
121
+ options = remote? ? remote_storage_options : routes.default_url_options
122
+ options = options.merge(opts)
123
+
124
+ if opts[:host].blank? && record.present?
125
+ organization = organization_for(record)
126
+ options[:host] = organization.host if organization
127
+ end
128
+
129
+ uri =
130
+ if options[:protocol] == "https" || options[:scheme] == "https"
131
+ URI::HTTPS.build(options)
132
+ else
133
+ URI::HTTP.build(options)
134
+ end
135
+
136
+ ActiveStorage::Current.url_options = { host: uri.to_s }
137
+ end
138
+
139
+ # Determines the organization for the passed record.
85
140
  #
86
- # @return [String] The converted representation URL
141
+ # @param record The record for which to fetch the organization
142
+ # @return [Decidim::Organization, nil] The organization for the record or
143
+ # `nil` if the organization cannot be determined
144
+ def organization_for(record)
145
+ if record.is_a?(Decidim::Organization)
146
+ record
147
+ elsif record.respond_to?(:organization)
148
+ record.organization
149
+ end
150
+ end
151
+
152
+ # Returns the URL for the given blob object.
153
+ #
154
+ # @param blob The blob object
155
+ # @param options Options for building the URL
156
+ # @return [String, nil] The URL to the blob object or `nil` if the blob is
157
+ # not defined.
158
+ def blob_url(**options)
159
+ return unless blob
160
+
161
+ if options[:only_path] || remote? || !asset_url_available?
162
+ routes.rails_blob_url(blob, **default_options.merge(options))
163
+ else
164
+ blob.url(**options)
165
+ end
166
+ end
167
+
168
+ # Returns a representation URL for the asset either directly through the
169
+ # storage service or through the Rails representation URL in case the
170
+ # path URL is requested or if the asset variant has not been processed yet
171
+ # and is not therefore yet stored at the storage service.
172
+ #
173
+ # @return [String] The representation URL for the image variant
87
174
  def representation_url(**options)
175
+ return rails_representation_url(**options) if options[:only_path] || remote?
176
+
177
+ representation_url = variant_url(**options)
178
+ return representation_url if representation_url.present?
179
+
180
+ # In case the representation has not been processed yet, it may not have
181
+ # a representation URL yet and it therefore needs to be served through
182
+ # the local representation URL for the first time (or until it has been
183
+ # processed).
184
+ if options[:host]
185
+ rails_representation_url(**options)
186
+ else
187
+ representation_url(**options.merge(only_path: true))
188
+ end
189
+ end
190
+
191
+ # Returns the local Rails representation URL meaning that the asset will
192
+ # be served through the service itself. This may be necessary if the asset
193
+ # variant (e.g. a thumbnail) has not been processed yet because the
194
+ # variant representation has not been requested before.
195
+ #
196
+ # Due to performance reasons it is advised to avoid requesting the assets
197
+ # through the Rails representation URLs when possible because that causes
198
+ # a lot of requests to the Rails backend and slowness to the service under
199
+ # heavy loads.
200
+ #
201
+ # Converts the variation URLs last part to the correct file extension in
202
+ # case the variation has a different format than the original image. The
203
+ # conversion needs to be only done for the Rails representation URLs
204
+ # because once the image is stored at the storage service, it already has
205
+ # the correct file extension.
206
+ #
207
+ # @param options The options for building the URL
208
+ # @return [String, nil] The converted representation URL or `nil` if the
209
+ # asset is not defined.
210
+ def rails_representation_url(**options)
211
+ return unless asset
212
+
88
213
  representation_url = routes.rails_representation_url(asset, **default_options.merge(options))
214
+
89
215
  variation = asset.try(:variation)
90
216
  return representation_url unless variation
91
217
 
92
218
  format = variation.try(:format)
93
219
  return representation_url unless format
220
+ return unless blob
94
221
 
95
- original_ext = File.extname(asset.blob.filename.to_s)
222
+ original_ext = File.extname(blob.filename.to_s)
96
223
  return representation_url if original_ext == ".#{format}"
97
224
 
98
- basename = File.basename(asset.blob.filename.to_s, original_ext)
225
+ basename = File.basename(blob.filename.to_s, original_ext)
99
226
  representation_url.sub(/#{basename}\.#{original_ext.tr(".", "")}$/, "#{basename}.#{format}")
100
227
  end
228
+
229
+ # Fetches the image variant's URL at the storage service if the variant
230
+ # has already been processed and is stored at the storage service. If the
231
+ # variant has not been processed yet, returns `nil` in which case the
232
+ # variant has to be served through the service's own representation URL
233
+ # causing it to be processed and stored at the storage service.
234
+ #
235
+ # @param options The options for building the URL
236
+ # @return [String, nil] The variant URL at the storage service or `nil` if
237
+ # the variant has not been processed yet and does not yet exist at the
238
+ # storage service or `nil` when the asset is not defined
239
+ def variant_url(**options)
240
+ return unless asset
241
+ return unless asset_url_available?
242
+ return unless asset_exist?
243
+
244
+ case asset
245
+ when ActiveStorage::VariantWithRecord
246
+ # This is used when `ActiveStorage.track_variants` is enabled through
247
+ # `config.active_storage.track_variants`. In case the variant has not
248
+ # been processed yet, the `#url` method would return nil.
249
+ #
250
+ # Note that if the `asset.processed?` returns `true`, the variant
251
+ # record has been created in the database but it does not mean that
252
+ # it has been uploaded to the storage service yet. Likely a bug in
253
+ # ActiveStorage but to be sure that the asset is uploaded to the
254
+ # storage service, we also check that.
255
+ asset.url(**options) if asset.processed?
256
+ else # ActiveStorage::Variant
257
+ # Check whether the variant exists at the storage service before
258
+ # returning its URL. Otherwise the URL would be returned even when the
259
+ # variant is not yet processed causing 404 errors for the images on
260
+ # the page.
261
+ #
262
+ # Note that the `ActiveStorage::Variant#url` method only accepts
263
+ # certain keyword arguments where as the other objects allow any
264
+ # keyword arguments.
265
+ possible_kwargs = asset.method(:url).parameters.select { |p| p[0] == :key }.map { |p| p[1] }
266
+ asset.url(**options.slice(*possible_kwargs))
267
+ end
268
+ end
269
+
270
+ # Determines if the asset exists at the storage service.
271
+ #
272
+ # @return [Boolean] A boolean answering the question "does this asset
273
+ # exist at the storage service?".
274
+ def asset_exist?
275
+ return false if asset.key.blank?
276
+
277
+ blob.service.exist?(asset.key)
278
+ end
279
+
280
+ # Determines if the current host is required to build the asset URL.
281
+ #
282
+ # @return [Boolean] A boolean indicating if the current host is required
283
+ # to build the asset URL.
284
+ def current_host_required?
285
+ return false unless blob
286
+ return false unless defined?(ActiveStorage::Service::DiskService)
287
+
288
+ blob.service.is_a?(ActiveStorage::Service::DiskService)
289
+ end
290
+
291
+ # Determines if the asset URL can be generated.
292
+ #
293
+ # @return [Boolean] A boolean indicating if the asset URL can be
294
+ # generated.
295
+ def asset_url_available?
296
+ # If the service is an external service, the URL can be generated
297
+ # regardless of the current host being set.
298
+ return true unless current_host_required?
299
+
300
+ # For the disk service, the URL can be only generated if the current
301
+ # host has been set.
302
+ ActiveStorage::Current.url_options&.dig(:host).present?
303
+ end
101
304
  end
102
305
  end
103
306
  end
@@ -10,7 +10,7 @@ shared_examples_for "attachable interface" do
10
10
 
11
11
  it "includes the attachment urls" do
12
12
  attachment_urls = response["attachments"].map { |attachment| attachment["url"] }
13
- expect(attachment_urls).to include(*attachments.map(&:url))
13
+ expect(attachment_urls).to include_blob_urls(*attachments.map(&:file).map(&:blob))
14
14
  end
15
15
  end
16
16
  end
@@ -4,7 +4,7 @@ module Decidim
4
4
  # This holds the decidim-core version.
5
5
  module Core
6
6
  def self.version
7
- "0.29.0.rc3"
7
+ "0.29.0.rc4"
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decidim-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.0.rc3
4
+ version: 0.29.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep Jaume Rey Peroy
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2024-07-22 00:00:00.000000000 Z
13
+ date: 2024-07-30 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: active_link_to
@@ -758,28 +758,28 @@ dependencies:
758
758
  requirements:
759
759
  - - '='
760
760
  - !ruby/object:Gem::Version
761
- version: 0.29.0.rc3
761
+ version: 0.29.0.rc4
762
762
  type: :development
763
763
  prerelease: false
764
764
  version_requirements: !ruby/object:Gem::Requirement
765
765
  requirements:
766
766
  - - '='
767
767
  - !ruby/object:Gem::Version
768
- version: 0.29.0.rc3
768
+ version: 0.29.0.rc4
769
769
  - !ruby/object:Gem::Dependency
770
770
  name: decidim-dev
771
771
  requirement: !ruby/object:Gem::Requirement
772
772
  requirements:
773
773
  - - '='
774
774
  - !ruby/object:Gem::Version
775
- version: 0.29.0.rc3
775
+ version: 0.29.0.rc4
776
776
  type: :development
777
777
  prerelease: false
778
778
  version_requirements: !ruby/object:Gem::Requirement
779
779
  requirements:
780
780
  - - '='
781
781
  - !ruby/object:Gem::Version
782
- version: 0.29.0.rc3
782
+ version: 0.29.0.rc4
783
783
  description: Adds core features so other engines can hook into the framework.
784
784
  email:
785
785
  - josepjaume@gmail.com