decidim-core 0.29.0 → 0.29.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/activity_cell.rb +0 -3
  3. data/app/cells/decidim/author/show.erb +5 -4
  4. data/app/cells/decidim/author_cell.rb +26 -0
  5. data/app/cells/decidim/card_s/show.erb +5 -3
  6. data/app/cells/decidim/content_blocks/stats_cell.rb +1 -1
  7. data/app/cells/decidim/diff_cell.rb +4 -0
  8. data/app/cells/decidim/endorsement_buttons_cell.rb +1 -1
  9. data/app/cells/decidim/newsletter_templates/image_text_cta_cell.rb +1 -1
  10. data/app/cells/decidim/translation_bar/show.erb +2 -2
  11. data/app/cells/decidim/translation_bar_cell.rb +1 -1
  12. data/app/commands/decidim/amendable/create_draft.rb +1 -0
  13. data/app/commands/decidim/destroy_account.rb +3 -0
  14. data/app/controllers/concerns/decidim/devise_authentication_methods.rb +1 -1
  15. data/app/controllers/concerns/decidim/direct_upload.rb +82 -0
  16. data/app/controllers/decidim/doorkeeper/credentials_controller.rb +1 -1
  17. data/app/controllers/decidim/links_controller.rb +1 -1
  18. data/app/controllers/decidim/profiles_controller.rb +4 -0
  19. data/app/forms/decidim/upload_validation_form.rb +1 -1
  20. data/app/helpers/concerns/decidim/user_role_checker.rb +46 -0
  21. data/app/helpers/decidim/cta_button_helper.rb +1 -1
  22. data/app/helpers/decidim/layout_helper.rb +28 -0
  23. data/app/helpers/decidim/map_helper.rb +6 -1
  24. data/app/helpers/decidim/sanitize_helper.rb +11 -2
  25. data/app/helpers/decidim/scopes_helper.rb +3 -2
  26. data/app/models/decidim/action_log.rb +11 -1
  27. data/app/models/decidim/attachment.rb +1 -1
  28. data/app/packs/src/decidim/append_redirect_url_to_modals.js +24 -14
  29. data/app/packs/src/decidim/direct_uploads/upload_field.js +21 -8
  30. data/app/packs/src/decidim/direct_uploads/upload_modal.js +3 -0
  31. data/app/packs/src/decidim/index.js +3 -0
  32. data/app/packs/src/decidim/remote_tooltips.js +38 -0
  33. data/app/packs/src/decidim/toggle.js +1 -1
  34. data/app/packs/src/decidim/tooltips.js +42 -22
  35. data/app/packs/stylesheets/decidim/_buttons.scss +1 -1
  36. data/app/packs/stylesheets/decidim/_labels.scss +1 -1
  37. data/app/packs/stylesheets/decidim/_modal_update.scss +4 -0
  38. data/app/packs/stylesheets/decidim/_profile.scss +1 -1
  39. data/app/packs/stylesheets/decidim/_progress-bar.scss +1 -1
  40. data/app/packs/stylesheets/decidim/legacy/conference-diploma.scss +2 -1
  41. data/app/presenters/decidim/attachment_presenter.rb +1 -1
  42. data/app/queries/decidim/last_activity.rb +16 -5
  43. data/app/services/decidim/base_diff_renderer.rb +26 -2
  44. data/app/services/decidim/email_notification_generator.rb +14 -5
  45. data/app/views/decidim/devise/omniauth_registrations/new.html.erb +1 -1
  46. data/app/views/decidim/offline/show.html.erb +1 -1
  47. data/app/views/decidim/pages/_tabbed.html.erb +2 -2
  48. data/app/views/layouts/decidim/_head.html.erb +1 -1
  49. data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
  50. data/config/locales/ar.yml +16 -1
  51. data/config/locales/bg.yml +0 -1
  52. data/config/locales/bn-BD.yml +1 -0
  53. data/config/locales/bs-BA.yml +98 -0
  54. data/config/locales/ca.yml +14 -10
  55. data/config/locales/cs.yml +7 -1
  56. data/config/locales/de.yml +20 -16
  57. data/config/locales/el.yml +7 -1
  58. data/config/locales/en.yml +5 -1
  59. data/config/locales/es-MX.yml +6 -2
  60. data/config/locales/es-PY.yml +6 -2
  61. data/config/locales/es.yml +12 -8
  62. data/config/locales/eu.yml +202 -185
  63. data/config/locales/fi-plain.yml +5 -1
  64. data/config/locales/fi.yml +40 -36
  65. data/config/locales/fr-CA.yml +7 -3
  66. data/config/locales/fr.yml +6 -2
  67. data/config/locales/ga-IE.yml +9 -0
  68. data/config/locales/gl.yml +8 -1
  69. data/config/locales/hu.yml +3 -4
  70. data/config/locales/id-ID.yml +8 -0
  71. data/config/locales/is-IS.yml +8 -1
  72. data/config/locales/it.yml +19 -0
  73. data/config/locales/ja.yml +18 -16
  74. data/config/locales/lb.yml +9 -0
  75. data/config/locales/lt.yml +5 -2
  76. data/config/locales/lv.yml +8 -0
  77. data/config/locales/nl.yml +10 -1
  78. data/config/locales/no.yml +9 -0
  79. data/config/locales/pl.yml +1 -2
  80. data/config/locales/pt-BR.yml +244 -1
  81. data/config/locales/pt.yml +14 -0
  82. data/config/locales/ro-RO.yml +319 -180
  83. data/config/locales/ru.yml +8 -0
  84. data/config/locales/sk.yml +9 -1
  85. data/config/locales/sv.yml +541 -96
  86. data/config/locales/tr-TR.yml +10 -1
  87. data/config/locales/uk.yml +8 -1
  88. data/config/locales/zh-CN.yml +9 -0
  89. data/config/locales/zh-TW.yml +8 -1
  90. data/config/routes.rb +1 -0
  91. data/decidim-core.gemspec +4 -1
  92. data/lib/decidim/api/functions/component_list.rb +1 -1
  93. data/lib/decidim/api/functions/participatory_space_finder_base.rb +11 -1
  94. data/lib/decidim/api/interfaces/participatory_space_interface.rb +1 -1
  95. data/lib/decidim/api/types/component_type.rb +7 -0
  96. data/lib/decidim/api/types/user_group_type.rb +4 -0
  97. data/lib/decidim/api/types/user_type.rb +4 -0
  98. data/lib/decidim/attributes/rich_text.rb +38 -0
  99. data/lib/decidim/attributes/time_with_zone.rb +11 -1
  100. data/lib/decidim/attributes.rb +2 -0
  101. data/lib/decidim/content_parsers/blob_parser.rb +93 -0
  102. data/lib/decidim/content_parsers.rb +1 -0
  103. data/lib/decidim/content_renderers/blob_renderer.rb +90 -0
  104. data/lib/decidim/content_renderers.rb +1 -0
  105. data/lib/decidim/core/engine.rb +35 -1
  106. data/lib/decidim/core/test/factories.rb +28 -0
  107. data/lib/decidim/core/test/shared_examples/authorable_interface_examples.rb +1 -1
  108. data/lib/decidim/core/test/shared_examples/comments_examples.rb +25 -2
  109. data/lib/decidim/core/test/shared_examples/system_endorse_resource_examples.rb +107 -9
  110. data/lib/decidim/core/version.rb +1 -1
  111. data/lib/decidim/core.rb +11 -0
  112. data/lib/decidim/diffy_extension.rb +18 -0
  113. data/lib/decidim/form_builder.rb +1 -1
  114. data/lib/decidim/map/autocomplete.rb +1 -0
  115. data/lib/decidim/organization_settings.rb +4 -1
  116. data/lib/decidim/participatory_space_user.rb +4 -0
  117. data/lib/decidim/query_extensions.rb +0 -26
  118. data/lib/decidim/settings_manifest.rb +2 -0
  119. data/lib/decidim/translatable_attributes.rb +6 -1
  120. data/lib/decidim/view_model.rb +1 -1
  121. data/lib/tasks/upgrade/decidim_attachments.rake +14 -0
  122. data/lib/tasks/upgrade/decidim_fix_categorization.rake +34 -8
  123. metadata +30 -7
@@ -61,6 +61,8 @@ tr:
61
61
  'false': 'Hayır'
62
62
  'true': 'Evet'
63
63
  date:
64
+ buttons:
65
+ select: seçmek
64
66
  formats:
65
67
  decidim_short: "%d/%m/%Y"
66
68
  decidim_short_with_month_name_short: "%d %b %Y"
@@ -373,6 +375,10 @@ tr:
373
375
  name: Organizasyon istatistikleri
374
376
  sub_hero:
375
377
  name: Alt ana afişi
378
+ core:
379
+ application_helper:
380
+ filter_category_values:
381
+ all: Herşey
376
382
  devise:
377
383
  omniauth_registrations:
378
384
  new:
@@ -943,7 +949,7 @@ tr:
943
949
  scopes:
944
950
  global: Küresel kapsamı
945
951
  picker:
946
- cancel: İptal etmek
952
+ cancel: İptal Et
947
953
  change: Seçili kapsamı değiştir
948
954
  choose: seçmek
949
955
  currently_selected: Şu anda seçili kapsam
@@ -1012,6 +1018,7 @@ tr:
1012
1018
  participatory_space_filters:
1013
1019
  filters:
1014
1020
  areas: alanlar
1021
+ scope: Kapsam
1015
1022
  select_an_area: Bir bölge seçin
1016
1023
  progress: "Süreç\nİlerleme"
1017
1024
  reference:
@@ -1308,6 +1315,8 @@ tr:
1308
1315
  x: X
1309
1316
  xing: Xing
1310
1317
  time:
1318
+ buttons:
1319
+ select: seçmek
1311
1320
  formats:
1312
1321
  day_of_month: "%b %d"
1313
1322
  day_of_week: "%a"
@@ -37,6 +37,8 @@ uk:
37
37
  'false': 'Ні'
38
38
  'true': 'Так'
39
39
  date:
40
+ buttons:
41
+ select: Оберіть
40
42
  formats:
41
43
  decidim_short: "%d.%m.%Y"
42
44
  decidim_with_month_name: "%d %B %Y"
@@ -191,6 +193,10 @@ uk:
191
193
  name: Статистика організації
192
194
  sub_hero:
193
195
  name: Під-багатирський банер
196
+ core:
197
+ application_helper:
198
+ filter_category_values:
199
+ all: Усі
194
200
  devise:
195
201
  omniauth_registrations:
196
202
  new:
@@ -410,7 +416,6 @@ uk:
410
416
  scopes:
411
417
  global: Всеохопний обсяг
412
418
  picker:
413
- cancel: Скасувати
414
419
  choose: Оберіть
415
420
  title: Оберіть %{field}
416
421
  prompt: Оберіть обсяг
@@ -548,6 +553,8 @@ uk:
548
553
  weibo: Сіна Вайбо
549
554
  xing: Xing
550
555
  time:
556
+ buttons:
557
+ select: Оберіть
551
558
  formats:
552
559
  day_of_month: "%b %d"
553
560
  day_of_week: "%a"
@@ -57,6 +57,8 @@ zh-CN:
57
57
  'false': '否'
58
58
  'true': '否'
59
59
  date:
60
+ buttons:
61
+ select: 选择
60
62
  formats:
61
63
  decidim_short: "%d/%m/%Y"
62
64
  decidim_with_day_and_month_name: "%A %d %b %Y"
@@ -318,6 +320,10 @@ zh-CN:
318
320
  name: 组织统计
319
321
  sub_hero:
320
322
  name: 子英雄广告
323
+ core:
324
+ application_helper:
325
+ filter_category_values:
326
+ all: 所有的
321
327
  devise:
322
328
  omniauth_registrations:
323
329
  new:
@@ -906,6 +912,7 @@ zh-CN:
906
912
  participatory_space_filters:
907
913
  filters:
908
914
  areas: 地区
915
+ scope: 范围
909
916
  select_an_area: 选择区域
910
917
  reference:
911
918
  reference: '引用: %{reference}'
@@ -1170,6 +1177,8 @@ zh-CN:
1170
1177
  whatsapp_web: WhatsApp
1171
1178
  xing: Xing
1172
1179
  time:
1180
+ buttons:
1181
+ select: 选择
1173
1182
  formats:
1174
1183
  day_of_month: "%b %d"
1175
1184
  day_of_week: "%a"
@@ -73,6 +73,8 @@ zh-TW:
73
73
  'false': '否'
74
74
  'true': '是'
75
75
  date:
76
+ buttons:
77
+ select: 選擇
76
78
  formats:
77
79
  decidim_short: "%d/%m/%Y"
78
80
  decidim_short_with_month_name_short: "%d %b %Y"
@@ -490,6 +492,9 @@ zh-TW:
490
492
  core:
491
493
  actions:
492
494
  unauthorized: 您無權限執行此操作.
495
+ application_helper:
496
+ filter_category_values:
497
+ all: 全部
493
498
  devise:
494
499
  omniauth_registrations:
495
500
  create:
@@ -803,7 +808,6 @@ zh-TW:
803
808
  title_required: 標題為必填欄位!
804
809
  uploaded: 已上傳
805
810
  validating: 驗證中...
806
- validation_error: 驗證錯誤!
807
811
  select_file: 選擇檔案
808
812
  upload_help:
809
813
  dropzone: 將檔案拖曳到此處或點擊按鈕上傳。
@@ -1342,6 +1346,7 @@ zh-TW:
1342
1346
  participatory_space_filters:
1343
1347
  filters:
1344
1348
  areas: 區域
1349
+ scope: 範圍
1345
1350
  select_an_area: 選擇一個地區
1346
1351
  public_participation:
1347
1352
  public_participation: 公開展示我的出席情況。
@@ -1743,6 +1748,8 @@ zh-TW:
1743
1748
  whatsapp_web: WhatsApp
1744
1749
  xing: Xing
1745
1750
  time:
1751
+ buttons:
1752
+ select: 選擇
1746
1753
  formats:
1747
1754
  day_of_month: "%b %d"
1748
1755
  day_of_week: "%a"
data/config/routes.rb CHANGED
@@ -137,6 +137,7 @@ Decidim::Core::Engine.routes.draw do
137
137
  get "group_members", to: "profiles#group_members", as: "profile_group_members"
138
138
  get "group_admins", to: "profiles#group_admins", as: "profile_group_admins"
139
139
  get "activity", to: "user_activities#index", as: "profile_activity"
140
+ get "tooltip", to: "profiles#tooltip", as: "profile_tooltip"
140
141
  resources :conversations, except: [:destroy], controller: "user_conversations", as: "profile_conversations"
141
142
  end
142
143
 
data/decidim-core.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
21
21
  }
22
22
  s.summary = "The core of the Decidim framework."
23
23
  s.description = "Adds core features so other engines can hook into the framework."
24
- s.license = "AGPL-3.0"
24
+ s.license = "AGPL-3.0-or-later"
25
25
  s.required_ruby_version = "~> 3.2.0"
26
26
 
27
27
  s.files = Dir.chdir(__dir__) do
@@ -31,6 +31,9 @@ Gem::Specification.new do |s|
31
31
  end
32
32
  end
33
33
 
34
+ # Lock Temporarily as it is failing in 0.29 branch. More info: https://github.com/rails/rails/pull/54264
35
+ s.add_dependency "concurrent-ruby", "= 1.3.4"
36
+
34
37
  s.add_dependency "active_link_to", "~> 1.0"
35
38
  s.add_dependency "acts_as_list", "~> 1.0"
36
39
  s.add_dependency "batch-loader", "~> 1.2"
@@ -27,7 +27,7 @@ module Decidim
27
27
  @query = Decidim::Component
28
28
  # remove default ordering if custom order required
29
29
  @query = @query.unscoped if args[:order]
30
- @query = @query.where(participatory_space:).published
30
+ @query = @query.where(participatory_space:)
31
31
  add_filter_keys(args[:filter])
32
32
  add_order_keys(args[:order].to_h)
33
33
  add_default_order
@@ -22,7 +22,17 @@ module Decidim
22
22
  args.compact.keys.each do |key|
23
23
  query[key] = args[key]
24
24
  end
25
- model_class.public_spaces.find_by(query)
25
+
26
+ @query =
27
+ if ctx[:current_user]&.admin?
28
+ model_class
29
+ elsif model_class.respond_to?(:visible_for)
30
+ model_class.visible_for(ctx[:current_user])
31
+ else
32
+ model_class.public_spaces
33
+ end
34
+
35
+ @query.find_by(query)
26
36
  end
27
37
  end
28
38
  end
@@ -23,7 +23,7 @@ module Decidim
23
23
  ParticipatorySpaceManifestPresenter.new(object.manifest, object.organization)
24
24
  end
25
25
 
26
- field :components, [ComponentInterface], null: true, description: "Lists the components this space contains." do
26
+ field :components, [ComponentInterface, { null: true }], null: true, description: "Lists the components this space contains." do
27
27
  argument :filter, ComponentInputFilter, "Provides several methods to filter the results", required: false
28
28
  argument :order, ComponentInputSort, "Provides several methods to order the results", required: false
29
29
  end
@@ -5,6 +5,13 @@ module Decidim
5
5
  class ComponentType < Decidim::Api::Types::BaseObject
6
6
  implements Decidim::Core::ComponentInterface
7
7
  description "A base component with no particular specificities."
8
+
9
+ def self.authorized?(object, context)
10
+ context[:component] = object
11
+ context[:current_component] = object
12
+
13
+ super && allowed_to?(:read, :component, object, context)
14
+ end
8
15
  end
9
16
  end
10
17
  end
@@ -59,6 +59,10 @@ module Decidim
59
59
  def members_count
60
60
  object.accepted_memberships.count
61
61
  end
62
+
63
+ def self.authorized?(object, context)
64
+ super && !object.blocked?
65
+ end
62
66
  end
63
67
  end
64
68
  end
@@ -62,6 +62,10 @@ module Decidim
62
62
  def groups
63
63
  object.accepted_user_groups
64
64
  end
65
+
66
+ def self.authorized?(object, context)
67
+ super && object.confirmed? && !object.blocked? && !object.deleted?
68
+ end
65
69
  end
66
70
  end
67
71
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Attributes
5
+ # Custom attributes value to convert rich text strings in the database, i.e.
6
+ # strings that originate from the editor.
7
+ class RichText < Decidim::Attributes::CleanString
8
+ def type
9
+ :"decidim/attributes/rich_text"
10
+ end
11
+
12
+ # Serializes the value to the database.
13
+ def serialize(value)
14
+ serialize_value(value)
15
+ end
16
+
17
+ private
18
+
19
+ # From form to database
20
+ def serialize_value(value)
21
+ return value unless value.is_a?(String)
22
+
23
+ context = {}
24
+ parsed = Decidim::ContentProcessor.parse_with_processor(:blob, value, context)
25
+ parsed.rewrite
26
+ end
27
+
28
+ # From database to form
29
+ def cast_value(value)
30
+ clean_string = super
31
+ return clean_string unless clean_string.is_a?(String)
32
+
33
+ renderer = Decidim::ContentProcessor.renderer_klass(:blob).constantize.new(clean_string)
34
+ renderer.render
35
+ end
36
+ end
37
+ end
38
+ end
@@ -5,6 +5,15 @@ module Decidim
5
5
  # Custom attributes value to parse a String representing a Time using
6
6
  # the app TimeZone.
7
7
  class TimeWithZone < ActiveModel::Type::DateTime
8
+ # Date format: 2020-06-20T, 2020-06-20, 20/06/2020T or 20/06/2020
9
+ # Time format: 10:20, 10:20:30 or 10:20:30.123456
10
+ ISO_DATETIME_WITHOUT_TIMEZONE = %r{
11
+ \A
12
+ ((\d{4})-(\d\d)-(\d\d)|(\d\d)/(\d\d)/(\d{4}))(?:T|\s)
13
+ (\d\d):(\d\d)(:(\d\d)(?:\.(\d{1,6})\d*)?)?
14
+ \z
15
+ }x
16
+
8
17
  def type
9
18
  :"decidim/attributes/time_with_zone"
10
19
  end
@@ -18,8 +27,9 @@ module Decidim
18
27
  rescue ArgumentError
19
28
  fallback = super
20
29
  return fallback unless fallback.is_a?(Time)
30
+ return Time.zone.parse(fallback.strftime("%F %T")) if ISO_DATETIME_WITHOUT_TIMEZONE.match?(value)
21
31
 
22
- Time.zone.parse(fallback.strftime("%F %T"))
32
+ ActiveSupport::TimeWithZone.new(fallback, Time.zone)
23
33
  end
24
34
  end
25
35
  end
@@ -5,6 +5,7 @@ module Decidim
5
5
  autoload :TimeWithZone, "decidim/attributes/time_with_zone"
6
6
  autoload :LocalizedDate, "decidim/attributes/localized_date"
7
7
  autoload :CleanString, "decidim/attributes/clean_string"
8
+ autoload :RichText, "decidim/attributes/rich_text"
8
9
  autoload :Blob, "decidim/attributes/blob"
9
10
  autoload :Array, "decidim/attributes/array"
10
11
  autoload :Hash, "decidim/attributes/hash"
@@ -27,6 +28,7 @@ module Decidim
27
28
  ActiveModel::Type.register(:"decidim/attributes/time_with_zone", Decidim::Attributes::TimeWithZone)
28
29
  ActiveModel::Type.register(:"decidim/attributes/localized_date", Decidim::Attributes::LocalizedDate)
29
30
  ActiveModel::Type.register(:"decidim/attributes/clean_string", Decidim::Attributes::CleanString)
31
+ ActiveModel::Type.register(:"decidim/attributes/rich_text", Decidim::Attributes::RichText)
30
32
  ActiveModel::Type.register(:"decidim/attributes/blob", Decidim::Attributes::Blob)
31
33
 
32
34
  ActiveModel::Type.register(:integer, Decidim::Attributes::Integer)
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ContentParsers
5
+ # Parses any blob URLs from the content and replaces them with references
6
+ # to those blobs.
7
+ class BlobParser < BaseParser
8
+ # Matches all possible URLs pointing to ActiveStorage::Blob objects.
9
+ #
10
+ # Possible routes:
11
+ # get "/blobs/redirect/:signed_id/*filename"
12
+ # get "/blobs/proxy/:signed_id/*filename"
13
+ # get "/blobs/:signed_id/*filename"
14
+ # get "/representations/redirect/:signed_blob_id/:variation_key/*filename"
15
+ # get "/representations/proxy/:signed_blob_id/:variation_key/*filename"
16
+ # get "/representations/:signed_blob_id/:variation_key/*filename"
17
+ # get "/disk/:encoded_key/*filename"
18
+ #
19
+ # See:
20
+ # https://github.com/rails/rails/blob/a7e379896552ce43b822385c03c37f2bd47739d3/activestorage/config/routes.rb#L5-L14
21
+ BLOB_REGEX = %r{
22
+ # Group 1: Host part
23
+ (
24
+ # Group 2: Domain and subpath part
25
+ https?://((?!/rails).)+
26
+ )?
27
+ /rails/active_storage
28
+ # Group 3: Blob path, representation path or disk service path
29
+ /(blobs/redirect|blobs/proxy|blobs|representations/redirect|representations/proxy|representations|disk)
30
+ # Group 4: Signed ID for blobs or encoded key for disk service
31
+ /([^/]+)
32
+ # Group 5: Variation part (only for representations)
33
+ (
34
+ # Group 6: Variation key for representations
35
+ /([\w.=-]+)
36
+ )?
37
+ # Group 7: Filename
38
+ /([\w.=-]+)
39
+ }x
40
+
41
+ def rewrite
42
+ replace_blobs(content)
43
+ end
44
+
45
+ private
46
+
47
+ def replace_blobs(text)
48
+ text.gsub(BLOB_REGEX) do |match|
49
+ type_part = Regexp.last_match(3)
50
+ key_part = Regexp.last_match(4)
51
+
52
+ variation_key = nil
53
+ blob =
54
+ if type_part == "disk"
55
+ # Disk service URL
56
+ decoded = ActiveStorage.verifier.verified(key_part, purpose: :blob_key)
57
+ ActiveStorage::Blob.find_by(key: decoded[:key]) if decoded
58
+ else
59
+ # Representation or blob
60
+ if type_part.start_with?("representations")
61
+ # Representation
62
+ variation_part = Regexp.last_match(6)
63
+ variation_key = generate_variation_key(variation_part)
64
+ end
65
+
66
+ ActiveStorage::Blob.find_signed(key_part)
67
+ end
68
+ next match unless blob
69
+
70
+ "#{blob.to_global_id}#{"/#{variation_key}" if variation_key}"
71
+ end
72
+ end
73
+
74
+ def generate_variation_key(variation_part)
75
+ # The variation part has to be decoded because it will eventually
76
+ # expire. This way we can preserve the variation information
77
+ # longer.
78
+ variation = ActiveStorage.verifier.verify(variation_part, purpose: :variation)
79
+ return unless variation
80
+
81
+ # Convert to base64 encoded JSON string for better representation within
82
+ # the URLs. This manually encoded part will not expire as it is
83
+ # persisted to the database.
84
+ Base64.strict_encode64(ActiveSupport::JSON.encode(variation))
85
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
86
+ # This happens if the variation key is already expired in which
87
+ # case it cannot be represented and instead a URL to the blob is
88
+ # created.
89
+ variation_part
90
+ end
91
+ end
92
+ end
93
+ end
@@ -3,6 +3,7 @@
3
3
  module Decidim
4
4
  module ContentParsers
5
5
  autoload :BaseParser, "decidim/content_parsers/base_parser"
6
+ autoload :BlobParser, "decidim/content_parsers/blob_parser"
6
7
  autoload :UserParser, "decidim/content_parsers/user_parser"
7
8
  autoload :UserGroupParser, "decidim/content_parsers/user_group_parser"
8
9
  autoload :HashtagParser, "decidim/content_parsers/hashtag_parser"
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module ContentRenderers
5
+ # A renderer that searches Global IDs representing blobs in content and
6
+ # replaces it with a URL to these blobs.
7
+ #
8
+ # e.g. gid://<APP_NAME>/ActiveStorage::Blob/1
9
+ #
10
+ # OR for representations
11
+ #
12
+ # e.g. gid://<APP_NAME>/ActiveStorage::Blob/1/<encoded variant transformations>
13
+ #
14
+ # The `<encoded variant transformations>` part of the URL is a Base64
15
+ # encoded string that contains an unencrypted JSON-encoded value about the
16
+ # blob transformations. This way the specific representations can be stored
17
+ # in the database without having these values expiring.
18
+ #
19
+ # @see BaseRenderer Examples of how to use a content renderer
20
+ class BlobRenderer < BaseRenderer
21
+ # Matches a global id representing a Decidim::User
22
+ GLOBAL_ID_REGEX = %r{(gid://[\w-]+/ActiveStorage::Blob/\d+)(/([\w=-]+))?}
23
+
24
+ # Replaces found Global IDs matching an existing blob with a URL to
25
+ # that blob. The Global IDs representing an invalid ActiveStorage::Blob
26
+ # are replaced with an empty string.
27
+ #
28
+ # @return [String] the content ready to display (contains HTML)
29
+ def render(_options = nil)
30
+ replace_pattern(content, GLOBAL_ID_REGEX)
31
+ end
32
+
33
+ protected
34
+
35
+ def replace_pattern(text, pattern)
36
+ return text unless text.respond_to?(:gsub)
37
+
38
+ text.gsub(pattern) do
39
+ blob_gid = Regexp.last_match(1)
40
+ variation_key = Regexp.last_match(3)
41
+
42
+ blob = GlobalID::Locator.locate(blob_gid)
43
+ if variation_key
44
+ variation = begin
45
+ ActiveSupport::JSON.decode(Base64.strict_decode64(variation_key))
46
+ rescue JSON::ParseError
47
+ variation_key
48
+ end
49
+ blob_url(blob, variation)
50
+ else
51
+ blob_url(blob)
52
+ end
53
+ rescue ActiveRecord::RecordNotFound => _e
54
+ ""
55
+ end
56
+ end
57
+
58
+ def blob_url(blob, variation = nil)
59
+ url = begin
60
+ if variation
61
+ blob.variant(variation).url
62
+ else
63
+ blob.url
64
+ end
65
+ rescue ArgumentError
66
+ # ArgumentError is raised in case the blob's service is set to
67
+ # ActiveStorage::Service::DiskService and
68
+ # `ActiveStorage::Current.url_options` is not set.
69
+ end
70
+ raise URI::InvalidURIError if url.blank?
71
+
72
+ url
73
+ rescue URI::InvalidURIError
74
+ local_blob_url(blob, variation)
75
+ end
76
+
77
+ def local_blob_url(blob, variation = nil)
78
+ if variation
79
+ routes.rails_representation_url(blob.variant(variation), only_path: true)
80
+ else
81
+ routes.rails_blob_url(blob, only_path: true)
82
+ end
83
+ end
84
+
85
+ def routes
86
+ @routes ||= Rails.application.routes.url_helpers
87
+ end
88
+ end
89
+ end
90
+ end
@@ -3,6 +3,7 @@
3
3
  module Decidim
4
4
  module ContentRenderers
5
5
  autoload :BaseRenderer, "decidim/content_renderers/base_renderer"
6
+ autoload :BlobRenderer, "decidim/content_renderers/blob_renderer"
6
7
  autoload :UserRenderer, "decidim/content_renderers/user_renderer"
7
8
  autoload :UserGroupRenderer, "decidim/content_renderers/user_group_renderer"
8
9
  autoload :HashtagRenderer, "decidim/content_renderers/hashtag_renderer"
@@ -239,6 +239,34 @@ module Decidim
239
239
  app.config.action_mailer.deliver_later_queue_name = :mailers
240
240
  end
241
241
 
242
+ initializer "decidim_core.active_storage", before: "active_storage.configs" do |app|
243
+ next if app.config.active_storage.service_urls_expire_in.present?
244
+
245
+ # Ensure that the ActiveStorage URLs are valid long enough because with
246
+ # the default configuration they would expire in 5 minutes which is a
247
+ # problem:
248
+ # a) for the backend blob URL replacement
249
+ # and
250
+ # b) for caching
251
+ #
252
+ # Note the limitations for each storage service regarding the signed URL
253
+ # expiration times. This limitation has to be also considered when
254
+ # defining a caching strategy, otherwise e.g. images or files may not
255
+ # display correctly when caching is enabled.
256
+ #
257
+ # ActiveStorage disk service (default): no limitation
258
+ #
259
+ # S3: maximum is 7 days from the creation of the URL
260
+ # https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html
261
+ #
262
+ # Google: maximum is 7 days (604800 seconds)
263
+ # https://cloud.google.com/storage/docs/access-control/signed-urls
264
+ #
265
+ # Azure: no limitation
266
+ # https://learn.microsoft.com/en-us/azure/storage/common/storage-sas-overview#best-practices-when-using-sas
267
+ app.config.active_storage.service_urls_expire_in = 7.days
268
+ end
269
+
242
270
  initializer "decidim_core.signed_global_id", after: "global_id" do |app|
243
271
  next if app.config.global_id.fetch(:expires_in, nil).present?
244
272
 
@@ -277,6 +305,12 @@ module Decidim
277
305
  app.config.exceptions_app = Decidim::Core::Engine.routes
278
306
  end
279
307
 
308
+ initializer "decidim_core.direct_uploader_paths", after: "decidim_core.exceptions_app" do |_app|
309
+ config.to_prepare do
310
+ ActiveStorage::DirectUploadsController.include Decidim::DirectUpload
311
+ end
312
+ end
313
+
280
314
  initializer "decidim_core.locales" do |app|
281
315
  app.config.i18n.fallbacks = true
282
316
  end
@@ -408,7 +442,7 @@ module Decidim
408
442
 
409
443
  initializer "decidim_core.content_processors" do |_app|
410
444
  Decidim.configure do |config|
411
- config.content_processors += [:user, :user_group, :hashtag, :link]
445
+ config.content_processors += [:user, :user_group, :hashtag, :link, :blob]
412
446
  end
413
447
  end
414
448
 
@@ -218,8 +218,15 @@ FactoryBot.define do
218
218
  end
219
219
 
220
220
  trait :deleted do
221
+ name { "" }
222
+ nickname { "" }
221
223
  email { "" }
224
+ delete_reason { "I want to delete my account" }
225
+ admin { false }
222
226
  deleted_at { Time.current }
227
+ avatar { nil }
228
+ personal_url { "" }
229
+ about { "" }
223
230
  end
224
231
 
225
232
  trait :admin_terms_accepted do
@@ -1022,4 +1029,25 @@ FactoryBot.define do
1022
1029
  end
1023
1030
  end
1024
1031
  end
1032
+
1033
+ factory :blob, class: "ActiveStorage::Blob" do
1034
+ transient do
1035
+ filepath { Decidim::Dev.asset("city.jpeg") }
1036
+ end
1037
+
1038
+ filename { File.basename(filepath) }
1039
+ content_type { MiniMime.lookup_by_filename(filepath)&.content_type || "text/plain" }
1040
+
1041
+ before(:create) do |object, evaluator|
1042
+ object.upload(File.open(evaluator.filepath))
1043
+ end
1044
+
1045
+ trait :image do
1046
+ filepath { Decidim::Dev.asset("city.jpeg") }
1047
+ end
1048
+
1049
+ trait :document do
1050
+ filepath { Decidim::Dev.asset("Exampledocument.pdf") }
1051
+ end
1052
+ end
1025
1053
  end
@@ -18,7 +18,7 @@ shared_examples_for "authorable interface" do
18
18
  end
19
19
 
20
20
  describe "with a regular user" do
21
- let(:author) { create(:user, organization: model.participatory_space.organization) }
21
+ let(:author) { create(:user, :confirmed, organization: model.participatory_space.organization) }
22
22
  let(:query) { "{ author { name } }" }
23
23
 
24
24
  before do