decidim-core 0.28.3 → 0.28.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) 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/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/nav_links/show.erb +3 -3
  10. data/app/cells/decidim/newsletter_templates/image_text_cta_cell.rb +1 -1
  11. data/app/cells/decidim/resource_types_filter/show.erb +11 -12
  12. data/app/cells/decidim/translation_bar/show.erb +3 -3
  13. data/app/cells/decidim/translation_bar_cell.rb +1 -1
  14. data/app/commands/decidim/amendable/create_draft.rb +1 -0
  15. data/app/commands/decidim/destroy_account.rb +3 -0
  16. data/app/controllers/concerns/decidim/devise_authentication_methods.rb +1 -1
  17. data/app/controllers/concerns/decidim/direct_upload.rb +82 -0
  18. data/app/controllers/decidim/doorkeeper/credentials_controller.rb +1 -1
  19. data/app/controllers/decidim/links_controller.rb +1 -1
  20. data/app/controllers/decidim/profiles_controller.rb +4 -0
  21. data/app/forms/decidim/upload_validation_form.rb +1 -1
  22. data/app/helpers/concerns/decidim/user_role_checker.rb +46 -0
  23. data/app/helpers/decidim/cta_button_helper.rb +1 -1
  24. data/app/helpers/decidim/layout_helper.rb +28 -0
  25. data/app/helpers/decidim/map_helper.rb +6 -1
  26. data/app/helpers/decidim/menu_helper.rb +1 -1
  27. data/app/helpers/decidim/sanitize_helper.rb +11 -2
  28. data/app/helpers/decidim/scopes_helper.rb +5 -2
  29. data/app/models/decidim/action_log.rb +11 -1
  30. data/app/models/decidim/attachment.rb +1 -1
  31. data/app/packs/src/decidim/a11y.js +11 -15
  32. data/app/packs/src/decidim/append_redirect_url_to_modals.js +24 -14
  33. data/app/packs/src/decidim/direct_uploads/upload_field.js +21 -8
  34. data/app/packs/src/decidim/direct_uploads/upload_modal.js +3 -0
  35. data/app/packs/src/decidim/index.js +3 -0
  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/_buttons.scss +1 -1
  40. data/app/packs/stylesheets/decidim/_modal_update.scss +4 -0
  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/legacy/conference-diploma.scss +2 -1
  44. data/app/presenters/decidim/attachment_presenter.rb +1 -1
  45. data/app/presenters/decidim/menu_item_presenter.rb +1 -1
  46. data/app/queries/decidim/last_activity.rb +16 -5
  47. data/app/services/decidim/base_diff_renderer.rb +26 -2
  48. data/app/services/decidim/email_notification_generator.rb +14 -5
  49. data/app/views/decidim/devise/omniauth_registrations/new.html.erb +1 -1
  50. data/app/views/decidim/offline/show.html.erb +1 -1
  51. data/app/views/decidim/pages/_tabbed.html.erb +5 -5
  52. data/app/views/decidim/shared/_filters.html.erb +5 -5
  53. data/app/views/decidim/shared/_orders.html.erb +3 -2
  54. data/app/views/decidim/shared/filters/_check_boxes_tree.html.erb +1 -1
  55. data/app/views/decidim/shared/filters/_collection.html.erb +1 -1
  56. data/app/views/layouts/decidim/_head.html.erb +1 -1
  57. data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
  58. data/app/views/layouts/decidim/shared/_layout_user_profile.html.erb +2 -2
  59. data/config/locales/ar.yml +12 -1
  60. data/config/locales/bg.yml +0 -1
  61. data/config/locales/bn-BD.yml +1 -0
  62. data/config/locales/bs-BA.yml +98 -0
  63. data/config/locales/ca.yml +14 -10
  64. data/config/locales/cs.yml +6 -1
  65. data/config/locales/de.yml +18 -14
  66. data/config/locales/el.yml +3 -1
  67. data/config/locales/en.yml +5 -1
  68. data/config/locales/es-MX.yml +6 -2
  69. data/config/locales/es-PY.yml +6 -2
  70. data/config/locales/es.yml +12 -8
  71. data/config/locales/eu.yml +202 -185
  72. data/config/locales/fi-plain.yml +5 -1
  73. data/config/locales/fi.yml +40 -36
  74. data/config/locales/fr-CA.yml +6 -2
  75. data/config/locales/fr.yml +5 -1
  76. data/config/locales/ga-IE.yml +5 -0
  77. data/config/locales/gl.yml +4 -1
  78. data/config/locales/hu.yml +1 -2
  79. data/config/locales/id-ID.yml +4 -0
  80. data/config/locales/is-IS.yml +4 -1
  81. data/config/locales/it.yml +40 -0
  82. data/config/locales/ja.yml +18 -16
  83. data/config/locales/lb.yml +5 -0
  84. data/config/locales/lt.yml +1 -2
  85. data/config/locales/lv.yml +4 -0
  86. data/config/locales/nl.yml +6 -1
  87. data/config/locales/no.yml +5 -0
  88. data/config/locales/pl.yml +1 -2
  89. data/config/locales/pt-BR.yml +199 -1
  90. data/config/locales/pt.yml +11 -0
  91. data/config/locales/ro-RO.yml +302 -180
  92. data/config/locales/ru.yml +4 -0
  93. data/config/locales/sk.yml +5 -1
  94. data/config/locales/sv.yml +452 -81
  95. data/config/locales/tr-TR.yml +6 -1
  96. data/config/locales/uk.yml +4 -1
  97. data/config/locales/zh-CN.yml +5 -0
  98. data/config/locales/zh-TW.yml +4 -1
  99. data/config/routes.rb +1 -0
  100. data/decidim-core.gemspec +4 -1
  101. data/lib/decidim/api/functions/component_list.rb +1 -1
  102. data/lib/decidim/api/functions/participatory_space_finder_base.rb +11 -1
  103. data/lib/decidim/api/interfaces/participatory_space_interface.rb +1 -1
  104. data/lib/decidim/api/types/component_type.rb +7 -0
  105. data/lib/decidim/api/types/user_group_type.rb +4 -0
  106. data/lib/decidim/api/types/user_type.rb +4 -0
  107. data/lib/decidim/attributes/rich_text.rb +38 -0
  108. data/lib/decidim/attributes/time_with_zone.rb +11 -1
  109. data/lib/decidim/attributes.rb +2 -0
  110. data/lib/decidim/content_parsers/blob_parser.rb +93 -0
  111. data/lib/decidim/content_parsers.rb +1 -0
  112. data/lib/decidim/content_renderers/blob_renderer.rb +90 -0
  113. data/lib/decidim/content_renderers.rb +1 -0
  114. data/lib/decidim/core/engine.rb +35 -1
  115. data/lib/decidim/core/test/factories.rb +28 -0
  116. data/lib/decidim/core/test/shared_examples/authorable_interface_examples.rb +1 -1
  117. data/lib/decidim/core/test/shared_examples/comments_examples.rb +25 -2
  118. data/lib/decidim/core/test/shared_examples/system_endorse_resource_examples.rb +112 -14
  119. data/lib/decidim/core/version.rb +1 -1
  120. data/lib/decidim/core.rb +11 -0
  121. data/lib/decidim/diffy_extension.rb +18 -0
  122. data/lib/decidim/form_builder.rb +1 -1
  123. data/lib/decidim/map/autocomplete.rb +1 -0
  124. data/lib/decidim/organization_settings.rb +4 -1
  125. data/lib/decidim/participatory_space_user.rb +4 -0
  126. data/lib/decidim/query_extensions.rb +0 -26
  127. data/lib/decidim/settings_manifest.rb +2 -0
  128. data/lib/decidim/translatable_attributes.rb +6 -1
  129. data/lib/decidim/view_model.rb +1 -1
  130. data/lib/tasks/upgrade/decidim_attachments.rake +14 -0
  131. data/lib/tasks/upgrade/decidim_fix_categorization.rake +34 -8
  132. metadata +30 -7
@@ -379,6 +379,10 @@ tr:
379
379
  name: Organizasyon istatistikleri
380
380
  sub_hero:
381
381
  name: Alt ana afişi
382
+ core:
383
+ application_helper:
384
+ filter_category_values:
385
+ all: Herşey
382
386
  devise:
383
387
  omniauth_registrations:
384
388
  new:
@@ -957,7 +961,7 @@ tr:
957
961
  scopes:
958
962
  global: Küresel kapsamı
959
963
  picker:
960
- cancel: İptal etmek
964
+ cancel: İptal Et
961
965
  change: Seçili kapsamı değiştir
962
966
  choose: seçmek
963
967
  currently_selected: Şu anda seçili kapsam
@@ -1027,6 +1031,7 @@ tr:
1027
1031
  participatory_space_filters:
1028
1032
  filters:
1029
1033
  areas: alanlar
1034
+ scope: Kapsam
1030
1035
  select_an_area: Bir bölge seçin
1031
1036
  progress: "Süreç\nİlerleme"
1032
1037
  reference:
@@ -191,6 +191,10 @@ uk:
191
191
  name: Статистика організації
192
192
  sub_hero:
193
193
  name: Під-багатирський банер
194
+ core:
195
+ application_helper:
196
+ filter_category_values:
197
+ all: Усі
194
198
  devise:
195
199
  omniauth_registrations:
196
200
  new:
@@ -417,7 +421,6 @@ uk:
417
421
  scopes:
418
422
  global: Всеохопний обсяг
419
423
  picker:
420
- cancel: Скасувати
421
424
  choose: Оберіть
422
425
  title: Оберіть %{field}
423
426
  prompt: Оберіть обсяг
@@ -324,6 +324,10 @@ zh-CN:
324
324
  name: 组织统计
325
325
  sub_hero:
326
326
  name: 子英雄广告
327
+ core:
328
+ application_helper:
329
+ filter_category_values:
330
+ all: 所有的
327
331
  devise:
328
332
  omniauth_registrations:
329
333
  new:
@@ -923,6 +927,7 @@ zh-CN:
923
927
  participatory_space_filters:
924
928
  filters:
925
929
  areas: 地区
930
+ scope: 范围
926
931
  select_an_area: 选择区域
927
932
  reference:
928
933
  reference: '引用: %{reference}'
@@ -500,6 +500,9 @@ zh-TW:
500
500
  actions:
501
501
  login_before_access: 請先登入您的帳戶再進行訪問.
502
502
  unauthorized: 您無權限執行此操作.
503
+ application_helper:
504
+ filter_category_values:
505
+ all: 全部
503
506
  devise:
504
507
  omniauth_registrations:
505
508
  create:
@@ -824,7 +827,6 @@ zh-TW:
824
827
  title_required: 標題為必填欄位!
825
828
  uploaded: 已上傳
826
829
  validating: 驗證中...
827
- validation_error: 驗證錯誤!
828
830
  select_file: 選擇檔案
829
831
  upload_help:
830
832
  dropzone: 將檔案拖曳到此處或點擊按鈕上傳。
@@ -1369,6 +1371,7 @@ zh-TW:
1369
1371
  participatory_space_filters:
1370
1372
  filters:
1371
1373
  areas: 區域
1374
+ scope: 範圍
1372
1375
  select_an_area: 選擇一個地區
1373
1376
  public_participation:
1374
1377
  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
@@ -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
  # Overrides
@@ -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"
@@ -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
 
@@ -275,6 +303,12 @@ module Decidim
275
303
  app.config.exceptions_app = Decidim::Core::Engine.routes
276
304
  end
277
305
 
306
+ initializer "decidim_core.direct_uploader_paths", after: "decidim_core.exceptions_app" do |_app|
307
+ config.to_prepare do
308
+ ActiveStorage::DirectUploadsController.include Decidim::DirectUpload
309
+ end
310
+ end
311
+
278
312
  initializer "decidim_core.locales" do |app|
279
313
  app.config.i18n.fallbacks = true
280
314
  end
@@ -411,7 +445,7 @@ module Decidim
411
445
 
412
446
  initializer "decidim_core.content_processors" do |_app|
413
447
  Decidim.configure do |config|
414
- config.content_processors += [:user, :user_group, :hashtag, :link]
448
+ config.content_processors += [:user, :user_group, :hashtag, :link, :blob]
415
449
  end
416
450
  end
417
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
@@ -494,6 +494,16 @@ shared_examples "comments" do
494
494
  expect(page).to have_selector("span.comments-count", text: "#{commentable.comments.count} comments")
495
495
  expect(page.find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}").value).to be_empty
496
496
  end
497
+
498
+ it "shows the entry in last activities" do
499
+ visit decidim.last_activities_path
500
+ expect(page).to have_content("New comment: #{content}")
501
+
502
+ within "#filters" do
503
+ find("a", class: "filter", text: "Comment", match: :first).click
504
+ end
505
+ expect(page).to have_content("New comment: #{content}")
506
+ end
497
507
  end
498
508
 
499
509
  context "when user adds a new comment with a link" do
@@ -603,7 +613,7 @@ shared_examples "comments" do
603
613
  end
604
614
 
605
615
  context "when the user has verified organizations" do
606
- let(:user_group) { create(:user_group, :verified) }
616
+ let(:user_group) { create(:user_group, :verified, organization:) }
607
617
  let(:content) { "This is a new comment" }
608
618
 
609
619
  before do
@@ -731,6 +741,20 @@ shared_examples "comments" do
731
741
  expect(page).to have_content("Edited")
732
742
  end
733
743
  end
744
+
745
+ it "has only one edit modal" do
746
+ expect(page).to have_css("#editCommentModal#{comment.id}", visible: :hidden, count: 1)
747
+ 3.times do |index|
748
+ sleep 2
749
+ within "#comment_#{comment.id}" do
750
+ page.find("[id^='dropdown-trigger']").click
751
+ click_on "Edit"
752
+ end
753
+ fill_in "edit_comment_#{comment.id}", with: " This comment has been edited #{1 + index} times"
754
+ click_on "Send"
755
+ end
756
+ expect(page).to have_css("#editCommentModal#{comment.id}", visible: :all, count: 1)
757
+ end
734
758
  end
735
759
  end
736
760
  end
@@ -1015,7 +1039,6 @@ shared_examples "comments blocked" do
1015
1039
  end
1016
1040
 
1017
1041
  context "when authenticated" do
1018
- let!(:organization) { create(:organization) }
1019
1042
  let!(:user) { create(:user, :confirmed, organization:) }
1020
1043
  let!(:comments) { create_list(:comment, 3, commentable:) }
1021
1044