decidim-core 0.29.1 → 0.29.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) 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/diff_cell.rb +4 -0
  7. data/app/cells/decidim/newsletter_templates/image_text_cta_cell.rb +1 -1
  8. data/app/cells/decidim/translation_bar/show.erb +2 -2
  9. data/app/cells/decidim/translation_bar_cell.rb +1 -1
  10. data/app/commands/decidim/destroy_account.rb +3 -0
  11. data/app/controllers/decidim/doorkeeper/credentials_controller.rb +1 -1
  12. data/app/controllers/decidim/links_controller.rb +1 -1
  13. data/app/controllers/decidim/profiles_controller.rb +4 -0
  14. data/app/helpers/concerns/decidim/user_role_checker.rb +46 -0
  15. data/app/helpers/decidim/cta_button_helper.rb +1 -1
  16. data/app/helpers/decidim/map_helper.rb +6 -1
  17. data/app/helpers/decidim/sanitize_helper.rb +11 -2
  18. data/app/models/decidim/attachment.rb +1 -1
  19. data/app/packs/src/decidim/append_redirect_url_to_modals.js +14 -6
  20. data/app/packs/src/decidim/direct_uploads/upload_field.js +21 -8
  21. data/app/packs/src/decidim/index.js +3 -0
  22. data/app/packs/src/decidim/remote_tooltips.js +38 -0
  23. data/app/packs/src/decidim/toggle.js +1 -1
  24. data/app/packs/src/decidim/tooltips.js +42 -22
  25. data/app/packs/stylesheets/decidim/_labels.scss +1 -1
  26. data/app/packs/stylesheets/decidim/_profile.scss +1 -1
  27. data/app/packs/stylesheets/decidim/_progress-bar.scss +1 -1
  28. data/app/packs/stylesheets/decidim/legacy/conference-diploma.scss +2 -1
  29. data/app/presenters/decidim/attachment_presenter.rb +1 -1
  30. data/app/services/decidim/base_diff_renderer.rb +26 -2
  31. data/app/services/decidim/email_notification_generator.rb +14 -5
  32. data/app/views/decidim/pages/_tabbed.html.erb +2 -2
  33. data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
  34. data/config/locales/ar.yml +16 -0
  35. data/config/locales/bn-BD.yml +1 -0
  36. data/config/locales/bs-BA.yml +98 -0
  37. data/config/locales/ca.yml +13 -9
  38. data/config/locales/cs.yml +5 -0
  39. data/config/locales/de.yml +18 -14
  40. data/config/locales/el.yml +7 -0
  41. data/config/locales/en.yml +4 -0
  42. data/config/locales/es-MX.yml +5 -1
  43. data/config/locales/es-PY.yml +5 -1
  44. data/config/locales/es.yml +11 -7
  45. data/config/locales/eu.yml +198 -181
  46. data/config/locales/fi-plain.yml +4 -0
  47. data/config/locales/fi.yml +39 -35
  48. data/config/locales/fr-CA.yml +6 -2
  49. data/config/locales/fr.yml +5 -1
  50. data/config/locales/ga-IE.yml +9 -0
  51. data/config/locales/gl.yml +8 -0
  52. data/config/locales/hu.yml +3 -3
  53. data/config/locales/id-ID.yml +8 -0
  54. data/config/locales/is-IS.yml +8 -1
  55. data/config/locales/it.yml +19 -0
  56. data/config/locales/ja.yml +15 -13
  57. data/config/locales/lb.yml +9 -0
  58. data/config/locales/lt.yml +5 -1
  59. data/config/locales/lv.yml +8 -0
  60. data/config/locales/nl.yml +10 -1
  61. data/config/locales/no.yml +9 -0
  62. data/config/locales/pl.yml +1 -1
  63. data/config/locales/pt-BR.yml +2 -1
  64. data/config/locales/pt.yml +14 -0
  65. data/config/locales/ro-RO.yml +258 -135
  66. data/config/locales/ru.yml +8 -0
  67. data/config/locales/sk.yml +9 -1
  68. data/config/locales/sv.yml +7 -7
  69. data/config/locales/tr-TR.yml +10 -1
  70. data/config/locales/uk.yml +8 -1
  71. data/config/locales/zh-CN.yml +9 -0
  72. data/config/locales/zh-TW.yml +8 -0
  73. data/config/routes.rb +1 -0
  74. data/decidim-core.gemspec +4 -1
  75. data/lib/decidim/api/functions/component_list.rb +1 -1
  76. data/lib/decidim/api/functions/participatory_space_finder_base.rb +11 -1
  77. data/lib/decidim/api/interfaces/participatory_space_interface.rb +1 -1
  78. data/lib/decidim/api/types/component_type.rb +7 -0
  79. data/lib/decidim/api/types/user_group_type.rb +4 -0
  80. data/lib/decidim/api/types/user_type.rb +4 -0
  81. data/lib/decidim/attributes/rich_text.rb +38 -0
  82. data/lib/decidim/attributes/time_with_zone.rb +11 -1
  83. data/lib/decidim/attributes.rb +2 -0
  84. data/lib/decidim/content_parsers/blob_parser.rb +93 -0
  85. data/lib/decidim/content_parsers.rb +1 -0
  86. data/lib/decidim/content_renderers/blob_renderer.rb +90 -0
  87. data/lib/decidim/content_renderers.rb +1 -0
  88. data/lib/decidim/core/engine.rb +29 -1
  89. data/lib/decidim/core/test/factories.rb +28 -0
  90. data/lib/decidim/core/test/shared_examples/authorable_interface_examples.rb +1 -1
  91. data/lib/decidim/core/test/shared_examples/comments_examples.rb +15 -2
  92. data/lib/decidim/core/version.rb +1 -1
  93. data/lib/decidim/diffy_extension.rb +18 -0
  94. data/lib/decidim/form_builder.rb +1 -1
  95. data/lib/decidim/map/autocomplete.rb +1 -0
  96. data/lib/decidim/participatory_space_user.rb +4 -0
  97. data/lib/decidim/query_extensions.rb +0 -26
  98. data/lib/decidim/settings_manifest.rb +2 -0
  99. data/lib/decidim/translatable_attributes.rb +6 -1
  100. data/lib/tasks/upgrade/decidim_fix_categorization.rake +34 -8
  101. metadata +28 -7
@@ -51,6 +51,8 @@ ru:
51
51
  'false': 'Нет'
52
52
  'true': 'Да'
53
53
  date:
54
+ buttons:
55
+ select: Выбрать
54
56
  formats:
55
57
  decidim_short: "%d.%m.%Y"
56
58
  decidim_with_month_name: "%d %B %Y"
@@ -246,6 +248,10 @@ ru:
246
248
  name: Статистика организации
247
249
  sub_hero:
248
250
  name: Под-богатырский баннер
251
+ core:
252
+ application_helper:
253
+ filter_category_values:
254
+ all: Все
249
255
  devise:
250
256
  omniauth_registrations:
251
257
  new:
@@ -709,6 +715,8 @@ ru:
709
715
  weibo: Сина Вейбо
710
716
  xing: Xing
711
717
  time:
718
+ buttons:
719
+ select: Выбрать
712
720
  formats:
713
721
  day_of_month: "%b %d"
714
722
  day_of_week: "%a"
@@ -67,6 +67,8 @@ sk:
67
67
  'false': 'Nie'
68
68
  'true': 'Áno'
69
69
  date:
70
+ buttons:
71
+ select: Vybrat
70
72
  formats:
71
73
  decidim_short: "%d/%m/%Y"
72
74
  decidim_short_with_month_name_short: "%d %b %Y"
@@ -336,6 +338,10 @@ sk:
336
338
  name: Štatistiky organizácie
337
339
  sub_hero:
338
340
  name: Pruh pod hlavným pruhom
341
+ core:
342
+ application_helper:
343
+ filter_category_values:
344
+ all: Všetko
339
345
  devise:
340
346
  omniauth_registrations:
341
347
  new:
@@ -810,7 +816,7 @@ sk:
810
816
  scopes:
811
817
  global: Globální oblast působnosti
812
818
  picker:
813
- cancel: zrušenie
819
+ cancel: Zrušiť
814
820
  choose: Vybrat
815
821
  title: Vyberte %{field}
816
822
  prompt: Vyberte oblast
@@ -1081,6 +1087,8 @@ sk:
1081
1087
  whatsapp_web: WhatsApp
1082
1088
  xing: Xing
1083
1089
  time:
1090
+ buttons:
1091
+ select: Vybrat
1084
1092
  formats:
1085
1093
  day_of_month: "%b %d"
1086
1094
  day_of_week: "%a"
@@ -594,7 +594,7 @@ sv:
594
594
  already_have_an_account?: Har du redan ett konto?
595
595
  log_in: Logga in
596
596
  newsletter: Få enstaka nyhetsbrev med relevant information
597
- newsletter_title: Kontakttillstånd
597
+ newsletter_title: Tillåtelse att kontakta mig
598
598
  sign_up: Skapa ett konto
599
599
  subtitle: Skapa ett konto för att delta i plattformen.
600
600
  terms: användarvillkoren
@@ -947,9 +947,9 @@ sv:
947
947
  index:
948
948
  badge_title: "%{name} märke"
949
949
  how: Hur kan du få det
950
- page_description: Märken visar att du varit aktiv som deltagare på plattformen och deltagit i behandlingen. Medan du upptäcker, deltar och samverkar på plattformen får du olika märken. Här är listan med märken och hur du kan få dem.
950
+ page_description: Märkena visar att du varit en aktiv deltagare på plattformen. Genom att utforska, delta och samverka på plattformen får du olika märken. Här är alla märken och hur du kan få dem.
951
951
  title: Märken
952
- description: Märken visar att du varit aktiv som deltagare på plattformen och deltagit i behandlingen. Medan du upptäcker, deltar och samverkar på plattformen får du olika märken.
952
+ description: Märkena visar att du varit en aktiv deltagare på plattformen. Genom att utforska, delta och samverka på plattformen får du olika märken.
953
953
  level: Nivå %{level}
954
954
  reached_top: Du har nått den högsta nivån för detta märke.
955
955
  title: Vilka är märkena?
@@ -1042,7 +1042,7 @@ sv:
1042
1042
  help:
1043
1043
  main_topic:
1044
1044
  default_page:
1045
- content: "<p>I %{organization} kan du delta och besluta i de deltagandeprocesser som visas i toppmenyn: processer, samråd, namninsamlingar och konsultationer.</p> <p>Inom varje område kan du delta på olika sätt: lämna förslag – enskilt eller med andra människor – delta i debatter, prioritera projekt att genomföra, delta i möten med mera.</p>\n"
1045
+ content: "<p>I %{organization} kan du delta och besluta i de deltagandeprocesser som visas i toppmenyn: processer, samråd, initiativ och konsultationer.</p> <p>Inom varje område kan du delta på olika sätt: lämna förslag – enskilt eller med andra människor – delta i debatter, prioritera projekt att genomföra, delta i möten med mera.</p>\n"
1046
1046
  title: Vad kan jag göra i %{organization}?
1047
1047
  description: Läs mer om %{organization}.
1048
1048
  title: Allmänt
@@ -1444,7 +1444,7 @@ sv:
1444
1444
  jump_to: 'Gå till:'
1445
1445
  search: Sök
1446
1446
  state:
1447
- active: Aktiva
1447
+ active: Pågående
1448
1448
  all: Alla
1449
1449
  future: Framtida
1450
1450
  past: Tidigare
@@ -1724,7 +1724,7 @@ sv:
1724
1724
  already_signed_out: Du är utloggad.
1725
1725
  new:
1726
1726
  log_in: Logga in
1727
- signed_in: Loggade in.
1727
+ signed_in: Du är inloggad.
1728
1728
  signed_out: Du är utloggad.
1729
1729
  shared:
1730
1730
  links:
@@ -1975,7 +1975,7 @@ sv:
1975
1975
  account: Konto
1976
1976
  authorizations: Auktoriseringar
1977
1977
  delete_my_account: Radera mitt konto
1978
- my_data: Mitt data
1978
+ my_data: Min data
1979
1979
  my_interests: Mina intressen
1980
1980
  notifications_settings: Meddelandeinställningar
1981
1981
  profile: Profil
@@ -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:
@@ -1341,6 +1346,7 @@ zh-TW:
1341
1346
  participatory_space_filters:
1342
1347
  filters:
1343
1348
  areas: 區域
1349
+ scope: 範圍
1344
1350
  select_an_area: 選擇一個地區
1345
1351
  public_participation:
1346
1352
  public_participation: 公開展示我的出席情況。
@@ -1742,6 +1748,8 @@ zh-TW:
1742
1748
  whatsapp_web: WhatsApp
1743
1749
  xing: Xing
1744
1750
  time:
1751
+ buttons:
1752
+ select: 選擇
1745
1753
  formats:
1746
1754
  day_of_month: "%b %d"
1747
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"