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.
- checksums.yaml +4 -4
- data/app/cells/decidim/activity_cell.rb +1 -4
- data/app/cells/decidim/author/show.erb +5 -4
- data/app/cells/decidim/author_cell.rb +26 -0
- data/app/cells/decidim/card_s/show.erb +5 -3
- data/app/cells/decidim/diff_cell.rb +4 -0
- data/app/cells/decidim/newsletter_templates/image_text_cta_cell.rb +1 -1
- data/app/cells/decidim/participatory_space_dropdown_metadata/show.erb +5 -3
- data/app/cells/decidim/resource_types_filter/show.erb +1 -1
- data/app/cells/decidim/resource_types_filter_cell.rb +6 -6
- data/app/cells/decidim/translation_bar/show.erb +3 -3
- data/app/cells/decidim/translation_bar_cell.rb +1 -1
- data/app/cells/decidim/user_activity/show.erb +1 -1
- data/app/commands/decidim/create_omniauth_registration.rb +14 -8
- data/app/commands/decidim/destroy_account.rb +3 -0
- data/app/commands/decidim/search.rb +14 -0
- data/app/controllers/decidim/doorkeeper/credentials_controller.rb +1 -1
- data/app/controllers/decidim/links_controller.rb +1 -1
- data/app/controllers/decidim/profiles_controller.rb +6 -2
- data/app/controllers/decidim/reports_controller.rb +1 -1
- data/app/controllers/decidim/user_activities_controller.rb +1 -1
- data/app/forms/decidim/account_form.rb +5 -2
- data/app/helpers/concerns/decidim/user_role_checker.rb +46 -0
- data/app/helpers/decidim/cta_button_helper.rb +1 -1
- data/app/helpers/decidim/map_helper.rb +6 -1
- data/app/helpers/decidim/orders_helper.rb +2 -1
- data/app/helpers/decidim/participatory_space_helpers.rb +1 -1
- data/app/helpers/decidim/sanitize_helper.rb +11 -2
- data/app/models/decidim/attachment.rb +1 -1
- data/app/models/decidim/user.rb +0 -4
- data/app/models/decidim/user_base_entity.rb +4 -0
- data/app/packs/src/decidim/append_redirect_url_to_modals.js +14 -6
- data/app/packs/src/decidim/direct_uploads/upload_field.js +21 -8
- data/app/packs/src/decidim/index.js +5 -0
- data/app/packs/src/decidim/map/provider/here.js +1 -1
- data/app/packs/src/decidim/remote_tooltips.js +38 -0
- data/app/packs/src/decidim/toggle.js +1 -1
- data/app/packs/src/decidim/tooltips.js +42 -22
- data/app/packs/stylesheets/decidim/_hashtags.scss +5 -0
- data/app/packs/stylesheets/decidim/_header.scss +20 -2
- data/app/packs/stylesheets/decidim/_profile.scss +1 -1
- data/app/packs/stylesheets/decidim/_progress-bar.scss +1 -1
- data/app/packs/stylesheets/decidim/application.scss +1 -0
- data/app/packs/stylesheets/decidim/legacy/conference-diploma.scss +2 -1
- data/app/presenters/decidim/attachment_presenter.rb +1 -1
- data/app/presenters/decidim/log/user_presenter.rb +1 -0
- data/app/services/decidim/base_diff_renderer.rb +28 -2
- data/app/services/decidim/email_notification_generator.rb +14 -5
- data/app/services/decidim/static_map_generator.rb +1 -1
- data/app/views/decidim/last_activities/index.html.erb +1 -1
- data/app/views/decidim/pages/_tabbed.html.erb +2 -2
- data/app/views/decidim/reported_mailer/hide.html.erb +1 -1
- data/app/views/decidim/reported_mailer/report.html.erb +1 -1
- data/app/views/decidim/searches/_count.html.erb +1 -1
- data/app/views/decidim/searches/_filters.html.erb +40 -38
- data/app/views/decidim/shared/_orders.html.erb +2 -2
- data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
- data/config/locales/ar.yml +55 -12
- data/config/locales/bg.yml +17 -8
- data/config/locales/bn-BD.yml +1 -0
- data/config/locales/bs-BA.yml +100 -0
- data/config/locales/ca-IT.yml +2115 -0
- data/config/locales/ca.yml +69 -22
- data/config/locales/cs.yml +62 -15
- data/config/locales/de.yml +67 -20
- data/config/locales/el.yml +17 -2
- data/config/locales/en.yml +47 -0
- data/config/locales/eo.yml +2 -0
- data/config/locales/es-MX.yml +61 -14
- data/config/locales/es-PY.yml +65 -18
- data/config/locales/es.yml +72 -25
- data/config/locales/eu.yml +308 -250
- data/config/locales/fi-plain.yml +50 -11
- data/config/locales/fi.yml +87 -48
- data/config/locales/fr-CA.yml +57 -10
- data/config/locales/fr.yml +55 -8
- data/config/locales/ga-IE.yml +11 -0
- data/config/locales/gl.yml +33 -2
- data/config/locales/hu.yml +17 -10
- data/config/locales/id-ID.yml +32 -3
- data/config/locales/is-IS.yml +18 -2
- data/config/locales/it.yml +84 -14
- data/config/locales/ja.yml +70 -23
- data/config/locales/lb.yml +32 -7
- data/config/locales/lt.yml +9 -3
- data/config/locales/lv.yml +26 -2
- data/config/locales/nl.yml +33 -6
- data/config/locales/no.yml +25 -0
- data/config/locales/pl.yml +15 -6
- data/config/locales/pt-BR.yml +18 -8
- data/config/locales/pt.yml +31 -0
- data/config/locales/ro-RO.yml +475 -207
- data/config/locales/ru.yml +33 -1
- data/config/locales/sk.yml +39 -7
- data/config/locales/sl.yml +4 -0
- data/config/locales/sr-CS.yml +2 -0
- data/config/locales/sv.yml +35 -16
- data/config/locales/tr-TR.yml +32 -8
- data/config/locales/uk.yml +20 -2
- data/config/locales/zh-CN.yml +27 -2
- data/config/locales/zh-TW.yml +14 -0
- data/config/routes.rb +1 -0
- data/decidim-core.gemspec +4 -1
- data/lib/decidim/api/functions/component_list.rb +1 -1
- data/lib/decidim/api/functions/participatory_space_finder_base.rb +11 -1
- data/lib/decidim/api/interfaces/participatory_space_interface.rb +1 -1
- data/lib/decidim/api/types/component_type.rb +7 -0
- data/lib/decidim/api/types/user_group_type.rb +4 -0
- data/lib/decidim/api/types/user_type.rb +4 -0
- data/lib/decidim/attributes/rich_text.rb +38 -0
- data/lib/decidim/attributes/time_with_zone.rb +16 -2
- data/lib/decidim/attributes.rb +2 -0
- data/lib/decidim/content_parsers/blob_parser.rb +95 -0
- data/lib/decidim/content_parsers/user_parser.rb +1 -1
- data/lib/decidim/content_parsers.rb +1 -0
- data/lib/decidim/content_renderers/blob_renderer.rb +90 -0
- data/lib/decidim/content_renderers.rb +1 -0
- data/lib/decidim/core/engine.rb +29 -1
- data/lib/decidim/core/test/factories.rb +28 -0
- data/lib/decidim/core/test/shared_examples/authorable_interface_examples.rb +1 -1
- data/lib/decidim/core/test/shared_examples/comments_examples.rb +15 -2
- data/lib/decidim/core/test/shared_examples/reports_examples.rb +48 -6
- data/lib/decidim/core/test/shared_examples/uncommentable_component_examples.rb +26 -0
- data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +26 -0
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/diffy_extension.rb +18 -0
- data/lib/decidim/form_builder.rb +1 -1
- data/lib/decidim/map/autocomplete.rb +1 -0
- data/lib/decidim/map/provider/dynamic_map/here.rb +1 -40
- data/lib/decidim/map/provider/static_map/here.rb +34 -0
- data/lib/decidim/nicknamizable.rb +1 -1
- data/lib/decidim/participatory_space_user.rb +4 -0
- data/lib/decidim/query_extensions.rb +0 -26
- data/lib/decidim/reportable.rb +6 -2
- data/lib/decidim/settings_manifest.rb +2 -0
- data/lib/decidim/translatable_attributes.rb +10 -1
- data/lib/decidim/view_model.rb +1 -0
- data/lib/tasks/upgrade/decidim_fix_categorization.rake +34 -8
- data/lib/tasks/upgrade/decidim_fix_nickname_uniqueness.rake +23 -20
- metadata +30 -8
- data/app/packs/src/decidim/vendor/leaflet-tilelayer-here.js +0 -212
data/config/locales/zh-CN.yml
CHANGED
@@ -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}'
|
data/config/locales/zh-TW.yml
CHANGED
@@ -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:)
|
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
|
-
|
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
|
@@ -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
|
-
|
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
|
-
|
36
|
+
ActiveSupport::TimeWithZone.new(fallback, Time.zone)
|
23
37
|
end
|
24
38
|
end
|
25
39
|
end
|
data/lib/decidim/attributes.rb
CHANGED
@@ -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
|
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"
|
data/lib/decidim/core/engine.rb
CHANGED
@@ -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
|