decidim-core 0.28.4 → 0.28.6

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 (141) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/activity_cell.rb +1 -4
  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/diff_cell.rb +4 -0
  7. data/app/cells/decidim/newsletter_templates/image_text_cta_cell.rb +1 -1
  8. data/app/cells/decidim/participatory_space_dropdown_metadata/show.erb +5 -3
  9. data/app/cells/decidim/resource_types_filter/show.erb +1 -1
  10. data/app/cells/decidim/resource_types_filter_cell.rb +6 -6
  11. data/app/cells/decidim/translation_bar/show.erb +3 -3
  12. data/app/cells/decidim/translation_bar_cell.rb +1 -1
  13. data/app/cells/decidim/user_activity/show.erb +1 -1
  14. data/app/commands/decidim/create_omniauth_registration.rb +14 -8
  15. data/app/commands/decidim/destroy_account.rb +3 -0
  16. data/app/commands/decidim/search.rb +14 -0
  17. data/app/controllers/decidim/doorkeeper/credentials_controller.rb +1 -1
  18. data/app/controllers/decidim/links_controller.rb +1 -1
  19. data/app/controllers/decidim/profiles_controller.rb +6 -2
  20. data/app/controllers/decidim/reports_controller.rb +1 -1
  21. data/app/controllers/decidim/user_activities_controller.rb +1 -1
  22. data/app/forms/decidim/account_form.rb +5 -2
  23. data/app/helpers/concerns/decidim/user_role_checker.rb +46 -0
  24. data/app/helpers/decidim/cta_button_helper.rb +1 -1
  25. data/app/helpers/decidim/map_helper.rb +6 -1
  26. data/app/helpers/decidim/orders_helper.rb +2 -1
  27. data/app/helpers/decidim/participatory_space_helpers.rb +1 -1
  28. data/app/helpers/decidim/sanitize_helper.rb +11 -2
  29. data/app/models/decidim/attachment.rb +1 -1
  30. data/app/models/decidim/user.rb +0 -4
  31. data/app/models/decidim/user_base_entity.rb +4 -0
  32. data/app/packs/src/decidim/append_redirect_url_to_modals.js +14 -6
  33. data/app/packs/src/decidim/direct_uploads/upload_field.js +21 -8
  34. data/app/packs/src/decidim/index.js +5 -0
  35. data/app/packs/src/decidim/map/provider/here.js +1 -1
  36. data/app/packs/src/decidim/remote_tooltips.js +38 -0
  37. data/app/packs/src/decidim/toggle.js +1 -1
  38. data/app/packs/src/decidim/tooltips.js +42 -22
  39. data/app/packs/stylesheets/decidim/_hashtags.scss +5 -0
  40. data/app/packs/stylesheets/decidim/_header.scss +20 -2
  41. data/app/packs/stylesheets/decidim/_profile.scss +1 -1
  42. data/app/packs/stylesheets/decidim/_progress-bar.scss +1 -1
  43. data/app/packs/stylesheets/decidim/application.scss +1 -0
  44. data/app/packs/stylesheets/decidim/legacy/conference-diploma.scss +2 -1
  45. data/app/presenters/decidim/attachment_presenter.rb +1 -1
  46. data/app/presenters/decidim/log/user_presenter.rb +1 -0
  47. data/app/services/decidim/base_diff_renderer.rb +28 -2
  48. data/app/services/decidim/email_notification_generator.rb +14 -5
  49. data/app/services/decidim/static_map_generator.rb +1 -1
  50. data/app/views/decidim/last_activities/index.html.erb +1 -1
  51. data/app/views/decidim/pages/_tabbed.html.erb +2 -2
  52. data/app/views/decidim/reported_mailer/hide.html.erb +1 -1
  53. data/app/views/decidim/reported_mailer/report.html.erb +1 -1
  54. data/app/views/decidim/searches/_count.html.erb +1 -1
  55. data/app/views/decidim/searches/_filters.html.erb +40 -38
  56. data/app/views/decidim/shared/_orders.html.erb +2 -2
  57. data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
  58. data/config/locales/ar.yml +55 -12
  59. data/config/locales/bg.yml +17 -8
  60. data/config/locales/bn-BD.yml +1 -0
  61. data/config/locales/bs-BA.yml +100 -0
  62. data/config/locales/ca-IT.yml +2115 -0
  63. data/config/locales/ca.yml +69 -22
  64. data/config/locales/cs.yml +62 -15
  65. data/config/locales/de.yml +67 -20
  66. data/config/locales/el.yml +17 -2
  67. data/config/locales/en.yml +47 -0
  68. data/config/locales/eo.yml +2 -0
  69. data/config/locales/es-MX.yml +61 -14
  70. data/config/locales/es-PY.yml +65 -18
  71. data/config/locales/es.yml +72 -25
  72. data/config/locales/eu.yml +308 -250
  73. data/config/locales/fi-plain.yml +50 -11
  74. data/config/locales/fi.yml +87 -48
  75. data/config/locales/fr-CA.yml +57 -10
  76. data/config/locales/fr.yml +55 -8
  77. data/config/locales/ga-IE.yml +11 -0
  78. data/config/locales/gl.yml +33 -2
  79. data/config/locales/hu.yml +17 -10
  80. data/config/locales/id-ID.yml +32 -3
  81. data/config/locales/is-IS.yml +18 -2
  82. data/config/locales/it.yml +84 -14
  83. data/config/locales/ja.yml +70 -23
  84. data/config/locales/lb.yml +32 -7
  85. data/config/locales/lt.yml +9 -3
  86. data/config/locales/lv.yml +26 -2
  87. data/config/locales/nl.yml +33 -6
  88. data/config/locales/no.yml +25 -0
  89. data/config/locales/pl.yml +15 -6
  90. data/config/locales/pt-BR.yml +18 -8
  91. data/config/locales/pt.yml +31 -0
  92. data/config/locales/ro-RO.yml +475 -207
  93. data/config/locales/ru.yml +33 -1
  94. data/config/locales/sk.yml +39 -7
  95. data/config/locales/sl.yml +4 -0
  96. data/config/locales/sr-CS.yml +2 -0
  97. data/config/locales/sv.yml +35 -16
  98. data/config/locales/tr-TR.yml +32 -8
  99. data/config/locales/uk.yml +20 -2
  100. data/config/locales/zh-CN.yml +27 -2
  101. data/config/locales/zh-TW.yml +14 -0
  102. data/config/routes.rb +1 -0
  103. data/decidim-core.gemspec +4 -1
  104. data/lib/decidim/api/functions/component_list.rb +1 -1
  105. data/lib/decidim/api/functions/participatory_space_finder_base.rb +11 -1
  106. data/lib/decidim/api/interfaces/participatory_space_interface.rb +1 -1
  107. data/lib/decidim/api/types/component_type.rb +7 -0
  108. data/lib/decidim/api/types/user_group_type.rb +4 -0
  109. data/lib/decidim/api/types/user_type.rb +4 -0
  110. data/lib/decidim/attributes/rich_text.rb +38 -0
  111. data/lib/decidim/attributes/time_with_zone.rb +16 -2
  112. data/lib/decidim/attributes.rb +2 -0
  113. data/lib/decidim/content_parsers/blob_parser.rb +95 -0
  114. data/lib/decidim/content_parsers/user_parser.rb +1 -1
  115. data/lib/decidim/content_parsers.rb +1 -0
  116. data/lib/decidim/content_renderers/blob_renderer.rb +90 -0
  117. data/lib/decidim/content_renderers.rb +1 -0
  118. data/lib/decidim/core/engine.rb +29 -1
  119. data/lib/decidim/core/test/factories.rb +28 -0
  120. data/lib/decidim/core/test/shared_examples/authorable_interface_examples.rb +1 -1
  121. data/lib/decidim/core/test/shared_examples/comments_examples.rb +15 -2
  122. data/lib/decidim/core/test/shared_examples/reports_examples.rb +48 -6
  123. data/lib/decidim/core/test/shared_examples/uncommentable_component_examples.rb +26 -0
  124. data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +26 -0
  125. data/lib/decidim/core/version.rb +1 -1
  126. data/lib/decidim/diffy_extension.rb +18 -0
  127. data/lib/decidim/form_builder.rb +1 -1
  128. data/lib/decidim/map/autocomplete.rb +1 -0
  129. data/lib/decidim/map/provider/dynamic_map/here.rb +1 -40
  130. data/lib/decidim/map/provider/static_map/here.rb +34 -0
  131. data/lib/decidim/nicknamizable.rb +1 -1
  132. data/lib/decidim/participatory_space_user.rb +4 -0
  133. data/lib/decidim/query_extensions.rb +0 -26
  134. data/lib/decidim/reportable.rb +6 -2
  135. data/lib/decidim/settings_manifest.rb +2 -0
  136. data/lib/decidim/translatable_attributes.rb +10 -1
  137. data/lib/decidim/view_model.rb +1 -0
  138. data/lib/tasks/upgrade/decidim_fix_categorization.rake +34 -8
  139. data/lib/tasks/upgrade/decidim_fix_nickname_uniqueness.rake +23 -20
  140. metadata +30 -8
  141. data/app/packs/src/decidim/vendor/leaflet-tilelayer-here.js +0 -212
@@ -25,6 +25,8 @@ zh-CN:
25
25
  password_confirmation: 确认您的密码
26
26
  personal_url: 个人网址
27
27
  remove_avatar: 删除头像
28
+ user_group:
29
+ avatar: 头像
28
30
  models:
29
31
  decidim/attachment_created_event: 附文
30
32
  decidim/component_published_event: 活动组件
@@ -57,6 +59,8 @@ zh-CN:
57
59
  'false': '否'
58
60
  'true': '否'
59
61
  date:
62
+ buttons:
63
+ select: 选择
60
64
  formats:
61
65
  decidim_short: "%d/%m/%Y"
62
66
  decidim_with_day_and_month_name: "%A %d %b %Y"
@@ -320,10 +324,16 @@ zh-CN:
320
324
  view_all: 查看全部
321
325
  metrics:
322
326
  name: 组织指标
327
+ participatory_space_metrics:
328
+ name: 指标
323
329
  stats:
324
330
  name: 组织统计
325
331
  sub_hero:
326
332
  name: 子英雄广告
333
+ core:
334
+ application_helper:
335
+ filter_category_values:
336
+ all: 所有的
327
337
  devise:
328
338
  omniauth_registrations:
329
339
  new:
@@ -708,6 +718,8 @@ zh-CN:
708
718
  new_conversation: 新建对话
709
719
  next: 下一个
710
720
  title: 对话
721
+ reply_form:
722
+ placeholder: 您的回复...
711
723
  show:
712
724
  back: 回到所有对话
713
725
  chat_with: 对话
@@ -722,11 +734,9 @@ zh-CN:
722
734
  participants:
723
735
  description: 组织中的活跃参与者人数
724
736
  object: 参与者
725
- title: 参加者
726
737
  users:
727
738
  description: 参加组织的人数
728
739
  object: 参与者
729
- title: 参加者
730
740
  newsletter_mailer:
731
741
  newsletter:
732
742
  unsubscribe: 选择不接收这种类型的电子邮件, <a href="%{link}" target="_blank" class="unsubscribe">取消订阅</a>。
@@ -823,6 +833,8 @@ zh-CN:
823
833
  conversations: 对话
824
834
  followers: 关注者
825
835
  following: 关注的问题
836
+ group_admins: 管理管理员
837
+ group_members: 管理成员
826
838
  groups: 群組
827
839
  members: 成员
828
840
  officialized: 官方参与者
@@ -831,6 +843,16 @@ zh-CN:
831
843
  info: 徽章是通过在平台上执行特定活动赚取的。
832
844
  title: 徽章
833
845
  user:
846
+ actions:
847
+ create_user_group: 创建组
848
+ edit_profile: 编辑配置文件
849
+ edit_user_group: 编辑群组资料
850
+ invite_user: Invite participant
851
+ join_user_group: 加入组的请求
852
+ leave_user_group: 离开组
853
+ manage_user_group_admins: 管理管理员
854
+ manage_user_group_users: 管理成员
855
+ resend_email_confirmation_instructions: 重新发送电子邮件确认说明
834
856
  create_user_group: 创建组
835
857
  edit_profile: 编辑配置文件
836
858
  edit_user_group: 编辑群组资料
@@ -916,6 +938,8 @@ zh-CN:
916
938
  report: 报告
917
939
  spam: 包含点击、广告、骗子或脚本机器人。
918
940
  title: 报告不恰当的内容
941
+ flag_user_modal:
942
+ close: 关闭
919
943
  floating_help:
920
944
  help: 帮助
921
945
  login_modal:
@@ -923,6 +947,7 @@ zh-CN:
923
947
  participatory_space_filters:
924
948
  filters:
925
949
  areas: 地区
950
+ scope: 范围
926
951
  select_an_area: 选择区域
927
952
  reference:
928
953
  reference: '引用: %{reference}'
@@ -31,6 +31,8 @@ zh-TW:
31
31
  personal_url: 個人 URL
32
32
  remove_avatar: 刪除頭像
33
33
  tos_agreement: 同意服務條款
34
+ user_group:
35
+ avatar: 頭像
34
36
  models:
35
37
  decidim/attachment_created_event: 附件
36
38
  decidim/component_published_event: 啟用組件
@@ -76,6 +78,8 @@ zh-TW:
76
78
  errors:
77
79
  file_size_too_large: 檔案太大。
78
80
  date:
81
+ buttons:
82
+ select: 選擇
79
83
  formats:
80
84
  decidim_short: "%d/%m/%Y"
81
85
  decidim_short_with_month_name_short: "%d %b %Y"
@@ -481,6 +485,8 @@ zh-TW:
481
485
  view_all: 檢視全部
482
486
  metrics:
483
487
  name: 組織指標
488
+ participatory_space_metrics:
489
+ name: 指標
484
490
  static_page:
485
491
  section:
486
492
  name: 節
@@ -500,6 +506,9 @@ zh-TW:
500
506
  actions:
501
507
  login_before_access: 請先登入您的帳戶再進行訪問.
502
508
  unauthorized: 您無權限執行此操作.
509
+ application_helper:
510
+ filter_category_values:
511
+ all: 全部
503
512
  devise:
504
513
  omniauth_registrations:
505
514
  create:
@@ -1236,6 +1245,8 @@ zh-TW:
1236
1245
  conversations: 對話
1237
1246
  followers: 追隨者
1238
1247
  following: 關注
1248
+ group_admins: 管理管理員
1249
+ group_members: 管理成員
1239
1250
  groups: 群組
1240
1251
  members: 成員
1241
1252
  officialized: 正式認證參與者
@@ -1251,9 +1262,11 @@ zh-TW:
1251
1262
  edit_user_group: 編輯群組資料
1252
1263
  invite_user: 邀請參與者
1253
1264
  join_user_group: 申請加入群組
1265
+ leave_user_group: 退出群組
1254
1266
  manage_user_group_admins: 管理管理員
1255
1267
  manage_user_group_users: 管理成員
1256
1268
  message: 訊息
1269
+ resend_email_confirmation_instructions: 重新發送電子郵件確認指示
1257
1270
  confirmation_instructions_sent: 電子郵件確認指示已發送.
1258
1271
  create_user_group: 建立群組
1259
1272
  edit_profile: 編輯個人資料
@@ -1368,6 +1381,7 @@ zh-TW:
1368
1381
  participatory_space_filters:
1369
1382
  filters:
1370
1383
  areas: 區域
1384
+ scope: 範圍
1371
1385
  select_an_area: 選擇一個地區
1372
1386
  public_participation:
1373
1387
  public_participation: 公開展示我的出席情況。
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.1.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.2.2"
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
@@ -14,12 +23,17 @@ module Decidim
14
23
  def cast_value(value)
15
24
  return value unless value.is_a?(String)
16
25
 
17
- Time.zone.strptime(value, I18n.t("time.formats.decidim_short"))
26
+ if Date._iso8601(value).present?
27
+ Time.zone.iso8601(value)
28
+ else
29
+ Time.zone.strptime(value, I18n.t("time.formats.decidim_short"))
30
+ end
18
31
  rescue ArgumentError
19
32
  fallback = super
20
33
  return fallback unless fallback.is_a?(Time)
34
+ return Time.zone.parse(fallback.strftime("%F %T")) if ISO_DATETIME_WITHOUT_TIMEZONE.match?(value)
21
35
 
22
- Time.zone.parse(fallback.strftime("%F %T"))
36
+ ActiveSupport::TimeWithZone.new(fallback, Time.zone)
23
37
  end
24
38
  end
25
39
  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
  # Overrides
@@ -0,0 +1,95 @@
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
+ (?<host_part>
24
+ # Group 2: Domain and subpath part
25
+ #{URI::DEFAULT_PARSER.make_regexp(%w(https http))}
26
+ )?
27
+ /rails/active_storage
28
+ # Group 3: Blob path, representation path or disk service path
29
+ /(?<type_part>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
+ /(?<key_part>[^/]+)
32
+ # Group 5: Variation part (only for representations)
33
+ (
34
+ # Group 6: Variation key for representations
35
+ /(?<variation_part>[\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
+ named_captures = Regexp.last_match.named_captures
50
+
51
+ type_part = named_captures["type_part"]
52
+ key_part = named_captures["key_part"]
53
+
54
+ variation_key = nil
55
+ blob =
56
+ if type_part == "disk"
57
+ # Disk service URL
58
+ decoded = ActiveStorage.verifier.verified(key_part, purpose: :blob_key)
59
+ ActiveStorage::Blob.find_by(key: decoded[:key]) if decoded
60
+ else
61
+ # Representation or blob
62
+ if type_part.start_with?("representations")
63
+ # Representation
64
+ variation_part = named_captures["variation_part"]
65
+ variation_key = generate_variation_key(variation_part)
66
+ end
67
+
68
+ ActiveStorage::Blob.find_signed(key_part)
69
+ end
70
+ next match unless blob
71
+
72
+ "#{blob.to_global_id}#{"/#{variation_key}" if variation_key}"
73
+ end
74
+ end
75
+
76
+ def generate_variation_key(variation_part)
77
+ # The variation part has to be decoded because it will eventually
78
+ # expire. This way we can preserve the variation information
79
+ # longer.
80
+ variation = ActiveStorage.verifier.verify(variation_part, purpose: :variation)
81
+ return unless variation
82
+
83
+ # Convert to base64 encoded JSON string for better representation within
84
+ # the URLs. This manually encoded part will not expire as it is
85
+ # persisted to the database.
86
+ Base64.strict_encode64(ActiveSupport::JSON.encode(variation))
87
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
88
+ # This happens if the variation key is already expired in which
89
+ # case it cannot be represented and instead a URL to the blob is
90
+ # created.
91
+ variation_part
92
+ end
93
+ end
94
+ end
95
+ end
@@ -49,7 +49,7 @@ module Decidim
49
49
 
50
50
  def existing_mentionables
51
51
  @existing_mentionables ||= mentionable_class.where(
52
- "decidim_organization_id = ? AND LOWER(nickname) IN (?)",
52
+ "decidim_organization_id = ? AND nickname IN (?)",
53
53
  current_organization.id,
54
54
  content_nicknames
55
55
  )
@@ -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"
@@ -237,6 +237,34 @@ module Decidim
237
237
  app.config.action_mailer.deliver_later_queue_name = :mailers
238
238
  end
239
239
 
240
+ initializer "decidim_core.active_storage", before: "active_storage.configs" do |app|
241
+ next if app.config.active_storage.service_urls_expire_in.present?
242
+
243
+ # Ensure that the ActiveStorage URLs are valid long enough because with
244
+ # the default configuration they would expire in 5 minutes which is a
245
+ # problem:
246
+ # a) for the backend blob URL replacement
247
+ # and
248
+ # b) for caching
249
+ #
250
+ # Note the limitations for each storage service regarding the signed URL
251
+ # expiration times. This limitation has to be also considered when
252
+ # defining a caching strategy, otherwise e.g. images or files may not
253
+ # display correctly when caching is enabled.
254
+ #
255
+ # ActiveStorage disk service (default): no limitation
256
+ #
257
+ # S3: maximum is 7 days from the creation of the URL
258
+ # https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html
259
+ #
260
+ # Google: maximum is 7 days (604800 seconds)
261
+ # https://cloud.google.com/storage/docs/access-control/signed-urls
262
+ #
263
+ # Azure: no limitation
264
+ # https://learn.microsoft.com/en-us/azure/storage/common/storage-sas-overview#best-practices-when-using-sas
265
+ app.config.active_storage.service_urls_expire_in = 7.days
266
+ end
267
+
240
268
  initializer "decidim_core.signed_global_id", after: "global_id" do |app|
241
269
  next if app.config.global_id.fetch(:expires_in, nil).present?
242
270
 
@@ -417,7 +445,7 @@ module Decidim
417
445
 
418
446
  initializer "decidim_core.content_processors" do |_app|
419
447
  Decidim.configure do |config|
420
- config.content_processors += [:user, :user_group, :hashtag, :link]
448
+ config.content_processors += [:user, :user_group, :hashtag, :link, :blob]
421
449
  end
422
450
  end
423
451
 
@@ -203,8 +203,15 @@ FactoryBot.define do
203
203
  end
204
204
 
205
205
  trait :deleted do
206
+ name { "" }
207
+ nickname { "" }
206
208
  email { "" }
209
+ delete_reason { "I want to delete my account" }
210
+ admin { false }
207
211
  deleted_at { Time.current }
212
+ avatar { nil }
213
+ personal_url { "" }
214
+ about { "" }
208
215
  end
209
216
 
210
217
  trait :admin_terms_accepted do
@@ -1001,4 +1008,25 @@ FactoryBot.define do
1001
1008
  end
1002
1009
  end
1003
1010
  end
1011
+
1012
+ factory :blob, class: "ActiveStorage::Blob" do
1013
+ transient do
1014
+ filepath { Decidim::Dev.asset("city.jpeg") }
1015
+ end
1016
+
1017
+ filename { File.basename(filepath) }
1018
+ content_type { MiniMime.lookup_by_filename(filepath)&.content_type || "text/plain" }
1019
+
1020
+ before(:create) do |object, evaluator|
1021
+ object.upload(File.open(evaluator.filepath))
1022
+ end
1023
+
1024
+ trait :image do
1025
+ filepath { Decidim::Dev.asset("city.jpeg") }
1026
+ end
1027
+
1028
+ trait :document do
1029
+ filepath { Decidim::Dev.asset("Exampledocument.pdf") }
1030
+ end
1031
+ end
1004
1032
  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