decidim-core 0.29.0.rc3 → 0.29.0.rc4

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.
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