decidim-core 0.29.0.rc3 → 0.29.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/address/online.erb +2 -2
  3. data/app/cells/decidim/address_cell.rb +4 -0
  4. data/app/cells/decidim/card_g/show.erb +1 -1
  5. data/app/cells/decidim/card_g_cell.rb +5 -2
  6. data/app/cells/decidim/card_l/image.erb +2 -2
  7. data/app/cells/decidim/card_l_cell.rb +5 -2
  8. data/app/cells/decidim/content_blocks/hero_cell.rb +1 -1
  9. data/app/cells/decidim/content_blocks/highlighted_content_banner/show.erb +1 -1
  10. data/app/cells/decidim/content_blocks/participatory_space_hero_cell.rb +2 -2
  11. data/app/cells/decidim/nav_links/show.erb +3 -3
  12. data/app/cells/decidim/resource_types_filter/show.erb +11 -12
  13. data/app/commands/decidim/create_omniauth_registration.rb +10 -4
  14. data/app/controllers/concerns/decidim/devise_controllers.rb +1 -0
  15. data/app/controllers/concerns/decidim/paginable.rb +1 -1
  16. data/app/controllers/decidim/application_controller.rb +1 -0
  17. data/app/helpers/decidim/menu_helper.rb +1 -1
  18. data/app/helpers/decidim/paginate_helper.rb +3 -5
  19. data/app/models/decidim/attachment.rb +8 -7
  20. data/app/models/decidim/component.rb +4 -1
  21. data/app/models/decidim/content_block.rb +2 -2
  22. data/app/models/decidim/user.rb +12 -12
  23. data/app/packs/src/decidim/a11y.js +11 -15
  24. data/app/packs/src/decidim/attachments/file_or_link_tabs.js +7 -3
  25. data/app/packs/src/decidim/input_character_counter.js +1 -1
  26. data/app/packs/stylesheets/decidim/_dropdown.scss +9 -9
  27. data/app/packs/stylesheets/decidim/_filters.scss +3 -1
  28. data/app/packs/stylesheets/decidim/_footer.scss +1 -1
  29. data/app/packs/stylesheets/decidim/_forms.scss +4 -4
  30. data/app/packs/stylesheets/decidim/_tooltip.scss +10 -10
  31. data/app/packs/stylesheets/decidim/editor.scss +1 -1
  32. data/app/presenters/decidim/menu_item_presenter.rb +1 -1
  33. data/app/services/decidim/open_data_exporter.rb +8 -7
  34. data/app/views/decidim/manifests/show.json.erb +4 -4
  35. data/app/views/decidim/pages/_tabbed.html.erb +3 -3
  36. data/app/views/decidim/shared/_filters.html.erb +5 -5
  37. data/app/views/decidim/shared/_orders.html.erb +3 -2
  38. data/app/views/decidim/shared/filters/_check_boxes_tree.html.erb +1 -1
  39. data/app/views/decidim/shared/filters/_collection.html.erb +1 -1
  40. data/app/views/layouts/decidim/_logo.html.erb +1 -1
  41. data/app/views/layouts/decidim/footer/_main.html.erb +1 -1
  42. data/app/views/layouts/decidim/footer/_main_intro.html.erb +1 -1
  43. data/app/views/layouts/decidim/header/_main_links_desktop.html.erb +1 -1
  44. data/app/views/layouts/decidim/header/_main_links_mobile_account.html.erb +1 -1
  45. data/app/views/layouts/decidim/header/_main_links_mobile_item_account.html.erb +1 -1
  46. data/app/views/layouts/decidim/shared/_layout_user_profile.html.erb +2 -2
  47. data/config/locales/ca.yml +3 -3
  48. data/config/locales/cs.yml +14 -0
  49. data/config/locales/fi-plain.yml +3 -3
  50. data/config/locales/fi.yml +28 -28
  51. data/config/locales/sv.yml +72 -64
  52. data/db/migrate/20181025082245_add_timestamps_to_components.rb +5 -1
  53. data/lib/decidim/asset_router/storage.rb +216 -13
  54. data/lib/decidim/core/test/shared_examples/attachable_interface_examples.rb +1 -1
  55. data/lib/decidim/core/test/shared_examples/follows_examples.rb +8 -3
  56. data/lib/decidim/core/test/shared_examples/paginated_resource_examples.rb +5 -5
  57. data/lib/decidim/core/version.rb +1 -1
  58. data/lib/tasks/upgrade/decidim_fix_categorization.rake +100 -0
  59. metadata +8 -8
@@ -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
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- shared_examples "follows" do
3
+ # When using these shared examples, make sure there are no prior requests within
4
+ # the same group of examples where this is included. Otherwise you may end up
5
+ # in race conditions that cause these to fail as explained at:
6
+ # https://github.com/decidim/decidim/pull/6161
7
+ shared_examples "followable content for users" do
4
8
  before do
9
+ switch_to_host(organization.host)
5
10
  login_as user, scope: :user
6
11
  end
7
12
 
@@ -34,9 +39,9 @@ shared_examples "follows" do
34
39
  end
35
40
  end
36
41
 
37
- shared_examples "follows with a component" do
42
+ shared_examples "followable content for users with a component" do
38
43
  include_context "with a component"
39
- include_examples "follows"
44
+ include_examples "followable content for users"
40
45
 
41
46
  context "when the user is following the followable's participatory space" do
42
47
  before do
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  shared_examples "a paginated resource" do
4
- let(:collection_size) { 30 }
4
+ let(:collection_size) { 50 }
5
5
 
6
6
  before do
7
7
  visit_component
8
8
  end
9
9
 
10
- it "lists 10 resources per page by default" do
11
- expect(page).to have_css(resource_selector, count: 10)
12
- expect(page).to have_css("[data-pages] [data-page]", count: 3)
10
+ it "lists 25 resources per page by default" do
11
+ expect(page).to have_css(resource_selector, count: 25)
12
+ expect(page).to have_css("[data-pages] [data-page]", count: 2)
13
13
  end
14
14
 
15
15
  it "results per page can be changed from the selector" do
16
16
  expect(page).to have_css("[data-pagination]")
17
17
 
18
18
  within "[data-pagination]" do
19
- page.find("summary", text: "10").click
19
+ page.find("summary", text: "25").click
20
20
  click_on "50"
21
21
  end
22
22
 
@@ -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"
8
8
  end
9
9
  end
10
10
  end
@@ -2,6 +2,106 @@
2
2
 
3
3
  namespace :decidim do
4
4
  namespace :upgrade do
5
+ namespace :clean do
6
+ desc "Removes all the invalid records from search, notifications, follows and action_logs"
7
+ task invalid_records: [
8
+ :"decidim:upgrade:clean:searchable_resources",
9
+ :"decidim:upgrade:clean:notifications",
10
+ :"decidim:upgrade:clean:follows",
11
+ :"decidim:upgrade:clean:action_logs"
12
+ ]
13
+
14
+ desc "Removes any action logs belonging to invalid resources"
15
+ task :action_logs, [] => :environment do
16
+ puts "=== Deleting Action logs\n"
17
+ invalid = 0
18
+ Decidim::ActionLog.find_each do |log|
19
+ log.participatory_space if log.participatory_space_type.present?
20
+ log.resource if log.resource_type.present?
21
+
22
+ if log.resource_type == "Decidim::Component" && log.resource.blank?
23
+ log.delete
24
+ invalid += 1
25
+ end
26
+
27
+ next if log.decidim_component_id.blank?
28
+ next if log.component.present?
29
+
30
+ log.delete
31
+ invalid += 1
32
+ rescue NameError
33
+ log.delete
34
+ invalid += 1
35
+ end
36
+ puts "===== Deleted #{invalid} invalid action logs\n"
37
+ end
38
+
39
+ desc "Removes any follows belonging to invalid resources"
40
+ task :follows, [] => :environment do
41
+ puts "=== Deleting Follows\n"
42
+ invalid = 0
43
+ Decidim::Follow.find_each do |follow|
44
+ follow.followable
45
+
46
+ next unless follow.followable.respond_to?(:component)
47
+ next if follow.followable.component.present?
48
+
49
+ # We attempt to remove any of the follows that refer to spaces or components that disappeared
50
+ follow.destroy
51
+ invalid += 1
52
+ rescue NameError
53
+ # We use delete as we do not want to call the hooks
54
+ follow.delete
55
+ invalid += 1
56
+ end
57
+ puts "===== Deleted #{invalid} invalid follows\n"
58
+ end
59
+
60
+ desc "Removes any notifications belonging to invalid resources"
61
+ task :notifications, [] => :environment do
62
+ puts "=== Deleting Notification\n"
63
+ invalid = 0
64
+ Decidim::Notification.find_each do |notification|
65
+ # Check if the resource class still exists
66
+ notification.resource
67
+ # Check if the event class still exists
68
+ notification.event_class_instance
69
+ rescue NameError
70
+ notification.destroy
71
+ invalid += 1
72
+ end
73
+ puts "===== Deleted #{invalid} invalid notifications\n"
74
+ end
75
+
76
+ desc "Removes any resources from search index that do not exist"
77
+ task :searchable_resources, [] => :environment do
78
+ puts "=== Deleting Searchable results\n"
79
+ puts "==== Deleting invalid spaces \n"
80
+ invalid = 0
81
+ Decidim::SearchableResource.where.not(decidim_participatory_space_type: nil).find_each do |search|
82
+ search.decidim_participatory_space
83
+ rescue NameError
84
+ search.destroy!
85
+ invalid += 1
86
+ end
87
+ puts "===== Deleted #{invalid} invalid spaces\n"
88
+
89
+ puts "==== Deleting invalid resources from search index \n"
90
+ invalid = 0
91
+ Decidim::SearchableResource.find_each do |search|
92
+ next unless search.resource.respond_to?(:component)
93
+ next if search.resource.component.present?
94
+
95
+ search.destroy
96
+ invalid += 1
97
+ rescue NameError
98
+ search.destroy!
99
+ invalid += 1
100
+ end
101
+ puts "===== Deleted #{invalid} invalid resources\n"
102
+ end
103
+ end
104
+
5
105
  desc "Removes orphan categorizations"
6
106
  task fix_orphan_categorizations: :environment do
7
107
  logger = Logger.new($stdout)
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
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-09-10 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
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
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
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
783
783
  description: Adds core features so other engines can hook into the framework.
784
784
  email:
785
785
  - josepjaume@gmail.com
@@ -2994,9 +2994,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
2994
2994
  version: 3.2.0
2995
2995
  required_rubygems_version: !ruby/object:Gem::Requirement
2996
2996
  requirements:
2997
- - - ">"
2997
+ - - ">="
2998
2998
  - !ruby/object:Gem::Version
2999
- version: 1.3.1
2999
+ version: '0'
3000
3000
  requirements: []
3001
3001
  rubygems_version: 3.4.10
3002
3002
  signing_key: