decidim-core 0.31.4 → 0.31.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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/amendable/amend_button_card/show.erb +2 -2
  3. data/app/cells/decidim/author_cell.rb +0 -4
  4. data/app/cells/decidim/content_blocks/cta_settings_form/show.erb +1 -1
  5. data/app/cells/decidim/content_blocks/hero_settings_form/show.erb +2 -2
  6. data/app/cells/decidim/content_blocks/highlighted_content_banner_cell.rb +1 -1
  7. data/app/cells/decidim/content_blocks/highlighted_content_banner_settings_form/show.erb +1 -1
  8. data/app/cells/decidim/content_blocks/highlighted_participatory_spaces_cell.rb +1 -1
  9. data/app/cells/decidim/content_blocks/html_cell.rb +1 -1
  10. data/app/cells/decidim/content_blocks/participatory_space_hero_settings_form/show.erb +1 -1
  11. data/app/cells/decidim/content_blocks/static_page/section_cell.rb +1 -1
  12. data/app/cells/decidim/content_blocks/static_page/section_settings_form/show.erb +3 -1
  13. data/app/cells/decidim/content_blocks/static_page/summary_cell.rb +1 -1
  14. data/app/cells/decidim/content_blocks/static_page/summary_settings_form/show.erb +3 -1
  15. data/app/cells/decidim/content_blocks/static_page/two_pane_section_cell.rb +2 -2
  16. data/app/cells/decidim/content_blocks/static_page/two_pane_section_settings_form/show.erb +4 -2
  17. data/app/cells/decidim/data_consent/category.erb +5 -5
  18. data/app/cells/decidim/resource_types_filter/show.erb +2 -2
  19. data/app/cells/decidim/upload_modal_cell.rb +5 -0
  20. data/app/commands/decidim/destroy_account.rb +12 -1
  21. data/app/commands/decidim/multiple_attachments_methods.rb +28 -27
  22. data/app/controllers/concerns/decidim/devise_controllers.rb +10 -0
  23. data/app/controllers/decidim/download_your_data_controller.rb +1 -1
  24. data/app/controllers/decidim/notifications_subscriptions_controller.rb +8 -0
  25. data/app/controllers/decidim/private_downloads_controller.rb +29 -0
  26. data/app/jobs/decidim/process_inactive_participant_job.rb +0 -7
  27. data/app/mailers/decidim/delete_user_mailer.rb +14 -0
  28. data/app/mailers/decidim/participants_account_mailer.rb +0 -16
  29. data/app/models/decidim/attachment.rb +20 -2
  30. data/app/models/decidim/authorization.rb +7 -0
  31. data/app/models/decidim/moderation.rb +1 -1
  32. data/app/models/decidim/participatory_space_private_user.rb +1 -1
  33. data/app/models/decidim/private_download.rb +61 -0
  34. data/app/models/decidim/private_export.rb +6 -0
  35. data/app/models/decidim/user_base_entity.rb +17 -2
  36. data/app/models/decidim/user_moderation.rb +1 -1
  37. data/app/packs/src/decidim/controllers/form_validator/form_validator.js +6 -6
  38. data/app/packs/src/decidim/controllers/form_validator/form_validator.test.js +23 -1
  39. data/app/packs/src/decidim/controllers/mention/controller.js +296 -140
  40. data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +120 -457
  41. data/app/packs/src/decidim/controllers/multiple_mentions/controller.js +68 -32
  42. data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +30 -23
  43. data/app/packs/src/decidim/direct_uploads/upload_field.js +4 -4
  44. data/app/packs/src/decidim/direct_uploads/upload_modal.js +11 -7
  45. data/app/packs/src/decidim/geocoding/reverse_geocoding.js +15 -5
  46. data/app/packs/src/decidim/geocoding/reverse_geocoding.test.js +197 -0
  47. data/app/packs/src/decidim/index.js +4 -3
  48. data/app/packs/src/decidim/sw/push-permissions.js +48 -13
  49. data/app/packs/src/decidim/sw/sw.js +1 -1
  50. data/app/packs/src/decidim/utilities/text.js +6 -6
  51. data/app/packs/stylesheets/decidim/_conversations.scss +14 -0
  52. data/app/packs/stylesheets/decidim/_editor_suggestions.scss +49 -0
  53. data/app/packs/stylesheets/decidim/_floating_help.scss +1 -1
  54. data/app/packs/stylesheets/decidim/_tom_select.scss +23 -0
  55. data/app/packs/stylesheets/decidim/application.scss +2 -0
  56. data/app/packs/stylesheets/decidim/editor.scss +2 -33
  57. data/app/packs/stylesheets/decidim/geocoding_addons.scss +10 -2
  58. data/app/presenters/decidim/menu_item_presenter.rb +9 -3
  59. data/app/presenters/decidim/stats_presenter.rb +1 -1
  60. data/app/services/decidim/notifications_subscriptions_persistor.rb +6 -0
  61. data/app/services/decidim/push_subscription_endpoint_validator.rb +34 -0
  62. data/app/services/decidim/send_push_notification.rb +5 -1
  63. data/app/uploaders/decidim/image_uploader.rb +1 -1
  64. data/app/views/decidim/delete_user_mailer/delete.html.erb +6 -0
  65. data/app/views/decidim/gamification/badges/index.html.erb +1 -1
  66. data/app/views/decidim/homepage/show.html.erb +1 -1
  67. data/app/views/decidim/last_activities/index.html.erb +1 -1
  68. data/app/views/decidim/messaging/conversations/_error_modal.html.erb +11 -19
  69. data/app/views/decidim/messaging/conversations/error.js.erb +12 -7
  70. data/app/views/decidim/newsletters/unsubscribe.html.erb +1 -1
  71. data/app/views/decidim/notifications_settings/show.html.erb +5 -5
  72. data/app/views/decidim/offline/show.html.erb +1 -1
  73. data/app/views/decidim/pages/index.html.erb +1 -1
  74. data/app/views/decidim/profiles/show.html.erb +1 -1
  75. data/app/views/decidim/shared/_resource_actions.html.erb +4 -4
  76. data/app/views/decidim/user_activities/index.html.erb +1 -1
  77. data/app/views/layouts/decidim/_wrapper.html.erb +2 -2
  78. data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
  79. data/app/views/layouts/decidim/shared/_layout_center.html.erb +1 -1
  80. data/app/views/layouts/decidim/shared/_layout_item.html.erb +1 -1
  81. data/app/views/layouts/decidim/shared/_layout_two_col.html.erb +1 -1
  82. data/config/locales/ar.yml +0 -5
  83. data/config/locales/bg.yml +0 -6
  84. data/config/locales/ca-IT.yml +10 -10
  85. data/config/locales/ca.yml +10 -10
  86. data/config/locales/cs.yml +4 -13
  87. data/config/locales/de.yml +5 -17
  88. data/config/locales/el.yml +0 -4
  89. data/config/locales/en.yml +9 -9
  90. data/config/locales/es-MX.yml +9 -9
  91. data/config/locales/es-PY.yml +9 -9
  92. data/config/locales/es.yml +9 -9
  93. data/config/locales/eu.yml +49 -49
  94. data/config/locales/fi-plain.yml +9 -9
  95. data/config/locales/fi.yml +11 -11
  96. data/config/locales/fr-CA.yml +10 -9
  97. data/config/locales/fr.yml +10 -9
  98. data/config/locales/gl.yml +0 -3
  99. data/config/locales/hu.yml +0 -5
  100. data/config/locales/id-ID.yml +0 -3
  101. data/config/locales/it.yml +11 -3
  102. data/config/locales/ja.yml +24 -21
  103. data/config/locales/lb.yml +0 -5
  104. data/config/locales/lt.yml +0 -5
  105. data/config/locales/lv.yml +0 -3
  106. data/config/locales/nl.yml +0 -5
  107. data/config/locales/no.yml +0 -5
  108. data/config/locales/pl.yml +2 -8
  109. data/config/locales/pt-BR.yml +3 -16
  110. data/config/locales/pt.yml +0 -5
  111. data/config/locales/ro-RO.yml +1 -14
  112. data/config/locales/sk.yml +1408 -4
  113. data/config/locales/sv.yml +3 -11
  114. data/config/locales/tr-TR.yml +0 -6
  115. data/config/locales/zh-CN.yml +0 -5
  116. data/config/locales/zh-TW.yml +0 -5
  117. data/config/routes.rb +1 -0
  118. data/lib/decidim/api/functions/user_entity_list.rb +2 -0
  119. data/lib/decidim/attachment_attributes.rb +58 -9
  120. data/lib/decidim/command.rb +1 -1
  121. data/lib/decidim/content_renderers/base_renderer.rb +112 -0
  122. data/lib/decidim/content_renderers/blob_renderer.rb +4 -7
  123. data/lib/decidim/content_renderers/mention_resource_renderer.rb +10 -6
  124. data/lib/decidim/content_renderers/resource_renderer.rb +16 -7
  125. data/lib/decidim/content_renderers/user_renderer.rb +11 -9
  126. data/lib/decidim/core/content_blocks/registry_manager.rb +4 -4
  127. data/lib/decidim/core/engine.rb +8 -0
  128. data/lib/decidim/core/test/factories.rb +3 -0
  129. data/lib/decidim/core/test/shared_examples/admin_resource_gallery_examples.rb +10 -10
  130. data/lib/decidim/core/test/shared_examples/comments_examples.rb +6 -6
  131. data/lib/decidim/core/version.rb +1 -1
  132. data/lib/decidim/map/autocomplete.rb +4 -3
  133. data/lib/decidim/searchable.rb +5 -0
  134. data/lib/decidim/view_model.rb +1 -1
  135. data/lib/tasks/decidim_mailers_tasks.rake +31 -9
  136. metadata +14 -10
  137. data/app/commands/decidim/gallery_methods.rb +0 -107
  138. data/app/packs/src/decidim/vendor/tribute.js +0 -1890
  139. data/app/packs/stylesheets/decidim/_tribute.scss +0 -36
  140. data/app/views/decidim/participants_account_mailer/removal_notification.html.erb +0 -11
@@ -422,8 +422,8 @@ sv:
422
422
  success: Ändringsförslaget har dragits tillbaka.
423
423
  wizard_step_form:
424
424
  steps:
425
- '1': Skapa ändringsförslag
426
- '2': Publicera ditt ändringsförslag
425
+ '1': Skapa din korrigering
426
+ '2': Publicera din korrigering
427
427
  anonymous_user: Anonym
428
428
  application:
429
429
  document:
@@ -556,7 +556,6 @@ sv:
556
556
  global:
557
557
  amendments_enabled: Aktivera ändringsförslag
558
558
  comments_enabled: Aktivera kommentarer
559
- comments_max_length: Maximal längd på kommentarer
560
559
  define_taxonomy_filters: Ställ in filter innan du använder den här inställningen.
561
560
  dummy_global_attribute1: Dummy Attribut 1
562
561
  dummy_global_attribute2: Dummy Attribut 2
@@ -1215,7 +1214,6 @@ sv:
1215
1214
  create:
1216
1215
  error: Konversationen har inte startat. Försök igen senare.
1217
1216
  error_modal:
1218
- close: Stäng fönster
1219
1217
  correct_errors: Vänligen korrigera felen och försök igen.
1220
1218
  intro: 'Det fanns följande fel med ditt meddelande:'
1221
1219
  ok: Ok
@@ -1341,6 +1339,7 @@ sv:
1341
1339
  own_activity: Min egen verksamhet, som när någon kommenterar mitt förslag eller nämner mig
1342
1340
  push_notifications: Push-meddelanden
1343
1341
  push_notifications_reminder: För att få aviseringar från plattformen måste du först tillåta dem i webbläsarens inställningar.
1342
+ push_notifications_unsupported_browser: Din webbläsare stöds inte.
1344
1343
  receive_notifications_about: Jag vill få notiser om
1345
1344
  update_notifications_settings: Spara ändringar
1346
1345
  update:
@@ -1526,13 +1525,6 @@ sv:
1526
1525
  log_in: logga in
1527
1526
  never_logged_in: har aldrig loggat in
1528
1527
  subject: Varning om inaktivt konto
1529
- removal_notification:
1530
- body: Ditt %{organization_name}-konto har tagits bort på grund av inaktivitet.
1531
- greetings_html: |
1532
- Vänliga hälsningar,<br>
1533
- %{organization_name}
1534
- hello: Hej %{username},
1535
- subject: Inaktivt konto raderat
1536
1528
  passwords:
1537
1529
  update:
1538
1530
  error: Det gick inte att uppdatera lösenordet.
@@ -202,10 +202,6 @@ tr:
202
202
  update_draft:
203
203
  error: Değişiklik taslağı güncellenirken bir hata oluştu.
204
204
  success: Değişiklik taslağı başarıyla güncellendi.
205
- wizard_step_form:
206
- steps:
207
- '1': Değişikliğinizi oluşturun
208
- '2': Değişikliğinizi yayınlayın
209
205
  anonymous_user: Anonim
210
206
  application:
211
207
  document:
@@ -305,7 +301,6 @@ tr:
305
301
  global:
306
302
  amendments_enabled: Değişiklikler etkinleştirildi
307
303
  comments_enabled: Yorumlar etkin
308
- comments_max_length: Maksimum yorum uzunluğu
309
304
  dummy_global_attribute1: Sahet Özellik 1
310
305
  dummy_global_attribute2: Sahte Özellik 2
311
306
  dummy_global_translatable_text: Sahte Çevrilebilir Metin
@@ -633,7 +628,6 @@ tr:
633
628
  modal_title: Yeni görüşme
634
629
  no_results: Sonuç bulunamadı
635
630
  error_modal:
636
- close: Pencereyi kapat
637
631
  correct_errors: Lütfen hataları düzeltip tekrar deneyin.
638
632
  intro: 'Mesajınızda aşağıdaki hatalar vardı:'
639
633
  ok: OK
@@ -176,9 +176,6 @@ zh-CN:
176
176
  update_draft:
177
177
  error: 更新修正草案时遇到问题。
178
178
  success: 修正草稿已成功更新。
179
- wizard_step_form:
180
- steps:
181
- '1': 创建您的修改
182
179
  anonymous_user: 匿名的
183
180
  application:
184
181
  document:
@@ -266,7 +263,6 @@ zh-CN:
266
263
  global:
267
264
  amendments_enabled: 修改已启用
268
265
  comments_enabled: 评论已启用
269
- comments_max_length: 评论最大长度
270
266
  dummy_global_attribute1: 虚拟属性 1
271
267
  dummy_global_attribute2: 虚拟属性 2
272
268
  dummy_global_translatable_text: 虚拟可翻译文本
@@ -546,7 +542,6 @@ zh-CN:
546
542
  modal_title: 新建对话
547
543
  no_results: 没有结果
548
544
  error_modal:
549
- close: 关闭模式
550
545
  correct_errors: 请更正错误,然后重试。
551
546
  intro: '您的消息有以下错误:'
552
547
  ok: 好的
@@ -293,9 +293,6 @@ zh-TW:
293
293
  success: 修正案草稿成功更新。
294
294
  withdraw:
295
295
  success: 修正案已成功撤回.
296
- wizard_step_form:
297
- steps:
298
- '1': 創建您的修正案
299
296
  anonymous_user: 匿名
300
297
  application:
301
298
  document:
@@ -409,7 +406,6 @@ zh-TW:
409
406
  global:
410
407
  amendments_enabled: 修改功能已啟用
411
408
  comments_enabled: 啟用評論功能
412
- comments_max_length: 評論最大長度
413
409
  dummy_global_attribute1: 虛擬屬性 1
414
410
  dummy_global_attribute2: 虛擬屬性 2
415
411
  dummy_global_translatable_text: 虛擬可翻譯文字
@@ -819,7 +815,6 @@ zh-TW:
819
815
  create:
820
816
  error: 對話未開始。請稍後再試。
821
817
  error_modal:
822
- close: 關閉視窗
823
818
  correct_errors: 請修正錯誤後再試一次。
824
819
  intro: '您的訊息有以下錯誤:'
825
820
  ok: 好的
data/config/routes.rb CHANGED
@@ -54,6 +54,7 @@ Decidim::Core::Engine.routes.draw do
54
54
  end
55
55
 
56
56
  authenticate(:user) do
57
+ resources :private_downloads, only: :show
57
58
  devise_scope :user do
58
59
  get "change_password" => "devise/passwords"
59
60
  put "apply_password" => "devise/passwords"
@@ -11,6 +11,7 @@ module Decidim
11
11
  #
12
12
  class UserEntityList
13
13
  include NeedsApiFilterAndOrder
14
+ include NeedsApiDefaultOrder
14
15
 
15
16
  def initialize
16
17
  @model_class = Decidim::UserBaseEntity
@@ -24,6 +25,7 @@ module Decidim
24
25
  .includes(avatar_attachment: :blob)
25
26
  add_filter_keys(args[:filter])
26
27
  add_order_keys(args[:order].to_h)
28
+ add_default_order
27
29
  @query
28
30
  end
29
31
  end
@@ -15,24 +15,39 @@ module Decidim
15
15
  #
16
16
  # name - The attribute's name
17
17
  #
18
- #
19
18
  # Example:
20
19
  #
21
- # attachment_attribute :photos
20
+ # attachments_attribute :photos
22
21
  # # This will create two attributes of the following types:
23
22
  # # attribute :photos, Array[Integer]
24
23
  # # attribute :add_photos, Array
25
- # # In addition, it will generate the getter method for the attribute
26
- # # returning an array of the Decidim::Attachment records.
24
+ # # In addition, it will generate:
25
+ # # - A setter that handles String (JSON/CSV), Integer, or Array inputs.
26
+ # # - A getter that falls back to add_photos when the attribute is blank.
27
+ # # - Private helpers for parsing and extracting IDs.
27
28
  #
28
29
  # Returns nothing.
29
- def attachments_attribute(name)
30
+ def attachments_attribute(name) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
30
31
  attribute name, Array[Integer]
31
32
  attribute :"add_#{name}", Array
32
33
 
33
- # Define the getter method that fetches the attachment records based on
34
- # their types. For Strings and Integers, assumes they are IDs and will
35
- # fetch the attachment record matching that ID.
34
+ # Setter: coerces String (JSON or comma-separated IDs) and bare Integer
35
+ # values into the expected Array[Integer] format before delegating to super.
36
+ define_method :"#{name}=" do |value|
37
+ case value
38
+ when String
39
+ parsed = send(:"parse_string_#{name}", value)
40
+ parsed.any? ? super(parsed) : super(value)
41
+ when Integer
42
+ super([value])
43
+ else
44
+ super(value)
45
+ end
46
+ end
47
+
48
+ # Getter: resolves stored integer IDs to Decidim::Attachment records,
49
+ # caching the result on the instance. Falls back to extracting IDs from
50
+ # add_<name> when the attribute itself is blank, then resolves those too.
36
51
  variable_name = "@#{name}_records"
37
52
  define_method name do
38
53
  return instance_variable_get(variable_name) if instance_variable_defined?(variable_name)
@@ -40,9 +55,15 @@ module Decidim
40
55
  original = @attributes[name.to_s].value_before_type_cast
41
56
  return original if original && !original.is_a?(Array)
42
57
 
58
+ ids = if super().blank? && send(:"add_#{name}").present?
59
+ send(:"extract_ids_from_add_#{name}")
60
+ else
61
+ super()
62
+ end
63
+
43
64
  instance_variable_set(
44
65
  variable_name,
45
- super().map do |attachment|
66
+ ids.map do |attachment|
46
67
  if attachment.is_a?(Integer)
47
68
  Decidim::Attachment.find_by(id: attachment)
48
69
  else
@@ -51,6 +72,34 @@ module Decidim
51
72
  end.compact
52
73
  )
53
74
  end
75
+
76
+ # Private helpers -------------------------------------------------------
77
+
78
+ define_method :"extract_ids_from_add_#{name}" do
79
+ send(:"add_#{name}")
80
+ .select { |item| item.is_a?(Hash) && (item[:id].present? || item["id"].present?) }
81
+ .map { |item| (item[:id] || item["id"]).to_i }
82
+ end
83
+
84
+ define_method :"parse_string_#{name}" do |value|
85
+ return [] if value.blank?
86
+
87
+ send(:"parse_#{name}_ids", value)
88
+ end
89
+
90
+ define_method :"parse_#{name}_ids" do |value|
91
+ ids = begin
92
+ Array(JSON.parse(value))
93
+ rescue JSON::ParserError
94
+ value.split(",").map(&:strip)
95
+ end
96
+
97
+ ids.map(&:to_i).reject(&:zero?)
98
+ end
99
+
100
+ private :"extract_ids_from_add_#{name}"
101
+ private :"parse_string_#{name}"
102
+ private :"parse_#{name}_ids"
54
103
  end
55
104
  end
56
105
  end
@@ -53,7 +53,7 @@ module Decidim
53
53
  protected
54
54
 
55
55
  def event_arguments
56
- raise "#{self.class} must implement #event_arguments"
56
+ raise "#{self.class} must implement #event_arguments (passed to #{event_namespace})"
57
57
  end
58
58
 
59
59
  def event_namespace
@@ -12,6 +12,16 @@ module Decidim
12
12
  class BaseRenderer
13
13
  include Decidim::ContentProcessor::Common
14
14
 
15
+ ReplacementContext = Struct.new(:placement, :node_name, :attribute_name, :ancestor_names, keyword_init: true) do
16
+ def text?
17
+ placement == :text
18
+ end
19
+
20
+ def attribute?
21
+ placement == :attribute
22
+ end
23
+ end
24
+
15
25
  # @return [String] the content to be formatted
16
26
  attr_reader :content
17
27
 
@@ -34,6 +44,108 @@ module Decidim
34
44
  def render(_options = nil)
35
45
  content
36
46
  end
47
+
48
+ protected
49
+
50
+ def replace_pattern_by_context(text, pattern, skip_ancestor_tags: %w(code pre script style), on_missing: "")
51
+ return text unless text.respond_to?(:gsub)
52
+
53
+ skip_ancestor_tags = Array(skip_ancestor_tags).map(&:to_s)
54
+
55
+ has_match = pattern.is_a?(String) ? text.include?(pattern) : pattern.match?(text)
56
+ return text unless has_match
57
+
58
+ fragment = html_fragment(text)
59
+ attr_modified = replace_pattern_in_attributes(fragment, pattern, skip_ancestor_tags:, on_missing:) do |match, context|
60
+ yield(match, context)
61
+ end
62
+ text_modified = replace_pattern_in_text_nodes(fragment, pattern, skip_ancestor_tags:, on_missing:) do |match, context|
63
+ yield(match, context)
64
+ end
65
+
66
+ return text unless attr_modified || text_modified
67
+
68
+ fragment.to_s
69
+ end
70
+
71
+ private
72
+
73
+ def replace_pattern_in_attributes(fragment, pattern, skip_ancestor_tags:, on_missing:)
74
+ modified = false
75
+ fragment.xpath(".//*").each do |node|
76
+ next if skip_replacement_for_node?(node, skip_ancestor_tags)
77
+
78
+ node.attribute_nodes.each do |attribute|
79
+ replaced_value = attribute.value.gsub(pattern) do |match|
80
+ replace_match(match, replacement_context(node, placement: :attribute, attribute_name: attribute.name), on_missing:) do |resolved_match, context|
81
+ yield(resolved_match, context)
82
+ end
83
+ end
84
+ unless replaced_value == attribute.value
85
+ attribute.value = replaced_value
86
+ modified = true
87
+ end
88
+ end
89
+ end
90
+ modified
91
+ end
92
+
93
+ def replace_pattern_in_text_nodes(fragment, pattern, skip_ancestor_tags:, on_missing:)
94
+ modified = false
95
+ fragment.xpath(".//text()").each do |node|
96
+ parent = node.parent
97
+ next if skip_replacement_for_node?(parent, skip_ancestor_tags)
98
+
99
+ original_text = node.text
100
+ has_node_match = pattern.is_a?(String) ? original_text.include?(pattern) : pattern.match?(original_text)
101
+ next unless has_node_match
102
+
103
+ doc = node.document
104
+ context = replacement_context(parent, placement: :text)
105
+ last_pos = 0
106
+
107
+ original_text.scan(pattern) do
108
+ m = Regexp.last_match
109
+ node.add_previous_sibling(Nokogiri::XML::Text.new(original_text[last_pos...m.begin(0)], doc)) if m.begin(0) > last_pos
110
+
111
+ replacement = replace_match(m[0], context, on_missing:) do |resolved_match, ctx|
112
+ yield(resolved_match, ctx)
113
+ end
114
+
115
+ Loofah.fragment(replacement.to_s).children.to_a.each do |child|
116
+ node.add_previous_sibling(child)
117
+ end
118
+
119
+ last_pos = m.end(0)
120
+ end
121
+
122
+ node.add_previous_sibling(Nokogiri::XML::Text.new(original_text[last_pos..], doc)) if last_pos < original_text.length
123
+ node.remove
124
+ modified = true
125
+ end
126
+ modified
127
+ end
128
+
129
+ def replace_match(match, context, on_missing:)
130
+ yield(match, context)
131
+ rescue ActiveRecord::RecordNotFound
132
+ on_missing.respond_to?(:call) ? on_missing.call(match, context) : on_missing
133
+ end
134
+
135
+ def replacement_context(node, placement:, attribute_name: nil)
136
+ ReplacementContext.new(
137
+ placement:,
138
+ node_name: node&.name,
139
+ attribute_name:,
140
+ ancestor_names: node ? node.ancestors.map(&:name) : []
141
+ )
142
+ end
143
+
144
+ def skip_replacement_for_node?(node, skip_ancestor_tags)
145
+ return false unless node
146
+
147
+ ([node.name] + node.ancestors.map(&:name)).any? { |name| skip_ancestor_tags.include?(name) }
148
+ end
37
149
  end
38
150
  end
39
151
  end
@@ -33,11 +33,10 @@ module Decidim
33
33
  protected
34
34
 
35
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)
36
+ replace_pattern_by_context(text, pattern) do |match, _context|
37
+ match_data = match.match(GLOBAL_ID_REGEX)
38
+ blob_gid = match_data[1]
39
+ variation_key = match_data[3]
41
40
 
42
41
  blob = GlobalID::Locator.locate(blob_gid)
43
42
  if variation_key
@@ -50,8 +49,6 @@ module Decidim
50
49
  else
51
50
  blob_url(blob)
52
51
  end
53
- rescue ActiveRecord::RecordNotFound => _e
54
- ""
55
52
  end
56
53
  end
57
54
 
@@ -27,17 +27,15 @@ module Decidim
27
27
  protected
28
28
 
29
29
  def replace_pattern(text, pattern, editor:)
30
- return text unless text.respond_to?(:gsub)
31
-
32
- text.gsub(pattern) do |resource_gid|
30
+ replace_pattern_by_context(text, pattern) do |resource_gid, context|
33
31
  resource = GlobalID::Locator.locate(resource_gid)
34
- if editor
32
+ if context.attribute?
33
+ render_resource_url(resource, editor:)
34
+ elsif editor
35
35
  render_editor(resource_gid, resource)
36
36
  else
37
37
  render_resource_link(resource)
38
38
  end
39
- rescue ActiveRecord::RecordNotFound => _e
40
- ""
41
39
  end
42
40
  end
43
41
 
@@ -50,6 +48,12 @@ module Decidim
50
48
  link_to mention_title(resource), resource_path(resource)
51
49
  end
52
50
 
51
+ def render_resource_url(resource, editor:)
52
+ return resource_path(resource) if editor
53
+
54
+ Decidim::ResourceLocatorPresenter.new(resource).url
55
+ end
56
+
53
57
  def mention_title(resource)
54
58
  title = presenter_for(resource).title
55
59
 
@@ -11,17 +11,26 @@ module Decidim
11
11
  #
12
12
  # @return [String] the content ready to display (contains HTML)
13
13
  def render(_options = nil)
14
- return content unless content.respond_to?(:gsub)
15
-
16
- content.gsub(regex) do |resource_gid|
14
+ replace_pattern_by_context(content, regex, on_missing: proc { |match, _| "~#{match.split("/").last}" }) do |resource_gid, context|
17
15
  resource = GlobalID::Locator.locate(resource_gid)
18
- resource.presenter.display_mention
19
- rescue ActiveRecord::RecordNotFound
20
- resource_id = resource_gid.split("/").last
21
- "~#{resource_id}"
16
+
17
+ if context.attribute?
18
+ resource_attribute_value(resource)
19
+ else
20
+ resource.presenter.display_mention
21
+ end
22
22
  end
23
23
  end
24
24
 
25
+ protected
26
+
27
+ def resource_attribute_value(resource)
28
+ presenter = resource.presenter
29
+ return presenter.profile_path if presenter.respond_to?(:profile_path)
30
+
31
+ Decidim::ResourceLocatorPresenter.new(resource).path
32
+ end
33
+
25
34
  def regex
26
35
  raise "Not implemented"
27
36
  end
@@ -24,17 +24,15 @@ module Decidim
24
24
  protected
25
25
 
26
26
  def replace_pattern(text, pattern, editor:)
27
- return text unless text.respond_to?(:gsub)
28
-
29
- text.gsub(pattern) do |mentionable_gid|
30
- mentionable = GlobalID::Locator.locate(mentionable_gid)
31
- if editor
32
- render_editor(mentionable)
27
+ replace_pattern_by_context(text, pattern) do |user_gid, context|
28
+ user = GlobalID::Locator.locate(user_gid)
29
+ if context.attribute?
30
+ render_profile_path(user)
31
+ elsif editor
32
+ render_editor(user)
33
33
  else
34
- render_text(mentionable)
34
+ render_text(user)
35
35
  end
36
- rescue ActiveRecord::RecordNotFound => _e
37
- ""
38
36
  end
39
37
  end
40
38
 
@@ -55,6 +53,10 @@ module Decidim
55
53
  presenter_for(user).display_mention
56
54
  end
57
55
  end
56
+
57
+ def render_profile_path(user)
58
+ presenter_for(user).profile_path
59
+ end
58
60
  end
59
61
  end
60
62
  end
@@ -51,11 +51,11 @@ module Decidim
51
51
  ]
52
52
 
53
53
  content_block.settings do |settings|
54
- settings.attribute :title, type: :text, translated: true
54
+ settings.attribute :title, type: :text, translated: true, required: true
55
55
  settings.attribute :short_description, type: :text, translated: true
56
- settings.attribute :action_button_title, type: :text, translated: true
57
- settings.attribute :action_button_subtitle, type: :text, translated: true
58
- settings.attribute :action_button_url, type: :text
56
+ settings.attribute :action_button_title, type: :text, translated: true, required: true
57
+ settings.attribute :action_button_subtitle, type: :text, translated: true, required: true
58
+ settings.attribute :action_button_url, type: :text, required: true
59
59
  end
60
60
 
61
61
  content_block.default!
@@ -495,6 +495,14 @@ module Decidim
495
495
  end
496
496
  end
497
497
 
498
+ initializer "decidim_core.delete_account" do
499
+ config.to_prepare do
500
+ ActiveSupport::Notifications.subscribe("decidim.destroy_account:after") do |_event_name, data|
501
+ Decidim::DeleteUserMailer.delete(user_email: data[:user_email], user_name: data[:user_name], locale: data[:locale], organization: data[:organization]).deliver_later
502
+ end
503
+ end
504
+ end
505
+
498
506
  initializer "decidim_core.add_cells_view_paths" do
499
507
  Cell::ViewModel.view_paths << Rails.root.join("app/views") # for partials
500
508
  Cell::ViewModel.view_paths << File.expand_path("#{Decidim::Core::Engine.root}/app/cells")
@@ -85,6 +85,8 @@ FactoryBot.define do
85
85
  "#{Faker::Lorem.sentence(word_count: 1, supplemental: true, random_words_to_add: 3)} #{n}"
86
86
  end
87
87
 
88
+ sequence(:component_position)
89
+
88
90
  factory :category, class: "Decidim::Category" do
89
91
  transient do
90
92
  skip_injection { false }
@@ -463,6 +465,7 @@ FactoryBot.define do
463
465
  manifest_name { "dummy" }
464
466
  published_at { Time.current }
465
467
  deleted_at { nil }
468
+ weight { generate(:component_position) }
466
469
  settings do
467
470
  {
468
471
  dummy_global_translatable_text: generate_localized_title(:dummy_global_translatable_text, skip_injection:),
@@ -87,7 +87,7 @@ end
87
87
 
88
88
  shared_examples "admin creates resource gallery" do
89
89
  context "when uploading images" do
90
- let(:uploaded_photos) do
90
+ let(:uploaded_attachments) do
91
91
  [
92
92
  {
93
93
  title: "Picture of the city",
@@ -99,10 +99,10 @@ shared_examples "admin creates resource gallery" do
99
99
  }
100
100
  ]
101
101
  end
102
- let(:photos) { [] }
102
+ let(:current_attachments) { [] }
103
103
 
104
104
  it "creates a gallery for the resource" do
105
- expect { command.call }.to change(Decidim::Attachment, :count).by(uploaded_photos.count)
105
+ expect { command.call }.to change(Decidim::Attachment, :count).by(uploaded_attachments.count)
106
106
 
107
107
  resource = resource_class.last
108
108
  expect(resource.photos.count).to eq(2)
@@ -111,7 +111,7 @@ shared_examples "admin creates resource gallery" do
111
111
  end
112
112
 
113
113
  context "when gallery is left blank" do
114
- let(:uploaded_photos) { [] }
114
+ let(:uploaded_attachments) { [] }
115
115
 
116
116
  it "broadcasts ok" do
117
117
  expect { command.call }.to broadcast(:ok)
@@ -122,7 +122,7 @@ end
122
122
 
123
123
  shared_examples "admin manages resource gallery" do
124
124
  context "when managing images" do
125
- let(:uploaded_photos) do
125
+ let(:uploaded_attachments) do
126
126
  [
127
127
  {
128
128
  title: "Picture of the city",
@@ -134,10 +134,10 @@ shared_examples "admin manages resource gallery" do
134
134
  }
135
135
  ]
136
136
  end
137
- let(:photos) { [] }
137
+ let(:current_attachments) { [] }
138
138
 
139
139
  it "creates a gallery for the resource" do
140
- expect { command.call }.to change(Decidim::Attachment, :count).by(uploaded_photos.count)
140
+ expect { command.call }.to change(Decidim::Attachment, :count).by(uploaded_attachments.count)
141
141
  resource = resource_class.last
142
142
  expect(resource.photos.count).to eq(2)
143
143
  last_attachment = Decidim::Attachment.last
@@ -145,7 +145,7 @@ shared_examples "admin manages resource gallery" do
145
145
  end
146
146
 
147
147
  context "when gallery is left blank" do
148
- let(:uploaded_photos) { [] }
148
+ let(:uploaded_attachments) { [] }
149
149
 
150
150
  it "broadcasts ok" do
151
151
  expect { command.call }.to broadcast(:ok)
@@ -155,8 +155,8 @@ shared_examples "admin manages resource gallery" do
155
155
  context "when images are removed" do
156
156
  let!(:image1) { create(:attachment, :with_image, attached_to: resource) }
157
157
  let!(:image2) { create(:attachment, :with_image, attached_to: resource) }
158
- let(:uploaded_photos) { [] }
159
- let(:current_photos) { [image1] }
158
+ let(:uploaded_attachments) { [] }
159
+ let(:current_attachments) { [image1.id] }
160
160
 
161
161
  it "to decrease the number of photos in the gallery" do
162
162
  expect(resource.attachments.count).to eq(2)
@@ -1000,16 +1000,16 @@ shared_examples "comments" do
1000
1000
  let(:content) { "A valid user mention: @#{mentioned_user.nickname}" }
1001
1001
 
1002
1002
  context "when text finish with a mention" do
1003
- it "shows the tribute container" do
1004
- expect(page).to have_css(".tribute-container", text: mentioned_user.name, wait: 10)
1003
+ it "shows the suggestions menu" do
1004
+ expect(page).to have_css(".editor-suggestions-item", text: "@#{mentioned_user.nickname} (#{mentioned_user.name})", wait: 10)
1005
1005
  end
1006
1006
  end
1007
1007
 
1008
1008
  context "when text contains a mention" do
1009
1009
  let(:content) { "A valid user mention: @#{mentioned_user.nickname}." }
1010
1010
 
1011
- it "shows the tribute container" do
1012
- expect(page).to have_no_css(".tribute-container", text: mentioned_user.name)
1011
+ it "hides the suggestions menu" do
1012
+ expect(page).to have_no_css(".editor-suggestions-item", text: "@#{mentioned_user.nickname} (#{mentioned_user.name})")
1013
1013
  end
1014
1014
  end
1015
1015
  end
@@ -1018,8 +1018,8 @@ shared_examples "comments" do
1018
1018
  let!(:mentioned_user) { create(:user, organization:) }
1019
1019
  let(:content) { "A unconfirmed user mention: @#{mentioned_user.nickname}" }
1020
1020
 
1021
- it "do not show the tribute container" do
1022
- expect(page).to have_no_css(".tribute-container", text: mentioned_user.name)
1021
+ it "does not show the suggestions menu" do
1022
+ expect(page).to have_no_css(".editor-suggestions-item", text: "@#{mentioned_user.nickname} (#{mentioned_user.name})")
1023
1023
  end
1024
1024
  end
1025
1025
  end