decidim-core 0.29.0 → 0.29.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/cells/decidim/activity_cell.rb +0 -3
- 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/content_blocks/stats_cell.rb +1 -1
- data/app/cells/decidim/diff_cell.rb +4 -0
- data/app/cells/decidim/endorsement_buttons_cell.rb +1 -1
- data/app/cells/decidim/newsletter_templates/image_text_cta_cell.rb +1 -1
- data/app/cells/decidim/translation_bar/show.erb +2 -2
- data/app/cells/decidim/translation_bar_cell.rb +1 -1
- data/app/commands/decidim/amendable/create_draft.rb +1 -0
- data/app/commands/decidim/destroy_account.rb +3 -0
- data/app/controllers/concerns/decidim/devise_authentication_methods.rb +1 -1
- data/app/controllers/concerns/decidim/direct_upload.rb +82 -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 +4 -0
- data/app/forms/decidim/upload_validation_form.rb +1 -1
- 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/layout_helper.rb +28 -0
- data/app/helpers/decidim/map_helper.rb +6 -1
- data/app/helpers/decidim/sanitize_helper.rb +11 -2
- data/app/helpers/decidim/scopes_helper.rb +3 -2
- data/app/models/decidim/action_log.rb +11 -1
- data/app/models/decidim/attachment.rb +1 -1
- data/app/packs/src/decidim/append_redirect_url_to_modals.js +24 -14
- data/app/packs/src/decidim/direct_uploads/upload_field.js +21 -8
- data/app/packs/src/decidim/direct_uploads/upload_modal.js +3 -0
- data/app/packs/src/decidim/index.js +3 -0
- 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/_buttons.scss +1 -1
- data/app/packs/stylesheets/decidim/_labels.scss +1 -1
- data/app/packs/stylesheets/decidim/_modal_update.scss +4 -0
- data/app/packs/stylesheets/decidim/_profile.scss +1 -1
- data/app/packs/stylesheets/decidim/_progress-bar.scss +1 -1
- data/app/packs/stylesheets/decidim/legacy/conference-diploma.scss +2 -1
- data/app/presenters/decidim/attachment_presenter.rb +1 -1
- data/app/queries/decidim/last_activity.rb +16 -5
- data/app/services/decidim/base_diff_renderer.rb +26 -2
- data/app/services/decidim/email_notification_generator.rb +14 -5
- data/app/views/decidim/devise/omniauth_registrations/new.html.erb +1 -1
- data/app/views/decidim/offline/show.html.erb +1 -1
- data/app/views/decidim/pages/_tabbed.html.erb +2 -2
- data/app/views/layouts/decidim/_head.html.erb +1 -1
- data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
- data/config/locales/ar.yml +16 -1
- data/config/locales/bg.yml +0 -1
- data/config/locales/bn-BD.yml +1 -0
- data/config/locales/bs-BA.yml +98 -0
- data/config/locales/ca.yml +14 -10
- data/config/locales/cs.yml +7 -1
- data/config/locales/de.yml +20 -16
- data/config/locales/el.yml +7 -1
- data/config/locales/en.yml +5 -1
- data/config/locales/es-MX.yml +6 -2
- data/config/locales/es-PY.yml +6 -2
- data/config/locales/es.yml +12 -8
- data/config/locales/eu.yml +202 -185
- data/config/locales/fi-plain.yml +5 -1
- data/config/locales/fi.yml +40 -36
- data/config/locales/fr-CA.yml +7 -3
- data/config/locales/fr.yml +6 -2
- data/config/locales/ga-IE.yml +9 -0
- data/config/locales/gl.yml +8 -1
- data/config/locales/hu.yml +3 -4
- data/config/locales/id-ID.yml +8 -0
- data/config/locales/is-IS.yml +8 -1
- data/config/locales/it.yml +19 -0
- data/config/locales/ja.yml +18 -16
- data/config/locales/lb.yml +9 -0
- data/config/locales/lt.yml +5 -2
- data/config/locales/lv.yml +8 -0
- data/config/locales/nl.yml +10 -1
- data/config/locales/no.yml +9 -0
- data/config/locales/pl.yml +1 -2
- data/config/locales/pt-BR.yml +244 -1
- data/config/locales/pt.yml +14 -0
- data/config/locales/ro-RO.yml +319 -180
- data/config/locales/ru.yml +8 -0
- data/config/locales/sk.yml +9 -1
- data/config/locales/sv.yml +541 -96
- data/config/locales/tr-TR.yml +10 -1
- data/config/locales/uk.yml +8 -1
- data/config/locales/zh-CN.yml +9 -0
- data/config/locales/zh-TW.yml +8 -1
- 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 +11 -1
- data/lib/decidim/attributes.rb +2 -0
- data/lib/decidim/content_parsers/blob_parser.rb +93 -0
- 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 +35 -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 +25 -2
- data/lib/decidim/core/test/shared_examples/system_endorse_resource_examples.rb +107 -9
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/core.rb +11 -0
- 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/organization_settings.rb +4 -1
- data/lib/decidim/participatory_space_user.rb +4 -0
- data/lib/decidim/query_extensions.rb +0 -26
- data/lib/decidim/settings_manifest.rb +2 -0
- data/lib/decidim/translatable_attributes.rb +6 -1
- data/lib/decidim/view_model.rb +1 -1
- data/lib/tasks/upgrade/decidim_attachments.rake +14 -0
- data/lib/tasks/upgrade/decidim_fix_categorization.rake +34 -8
- metadata +30 -7
data/config/locales/tr-TR.yml
CHANGED
@@ -61,6 +61,8 @@ tr:
|
|
61
61
|
'false': 'Hayır'
|
62
62
|
'true': 'Evet'
|
63
63
|
date:
|
64
|
+
buttons:
|
65
|
+
select: seçmek
|
64
66
|
formats:
|
65
67
|
decidim_short: "%d/%m/%Y"
|
66
68
|
decidim_short_with_month_name_short: "%d %b %Y"
|
@@ -373,6 +375,10 @@ tr:
|
|
373
375
|
name: Organizasyon istatistikleri
|
374
376
|
sub_hero:
|
375
377
|
name: Alt ana afişi
|
378
|
+
core:
|
379
|
+
application_helper:
|
380
|
+
filter_category_values:
|
381
|
+
all: Herşey
|
376
382
|
devise:
|
377
383
|
omniauth_registrations:
|
378
384
|
new:
|
@@ -943,7 +949,7 @@ tr:
|
|
943
949
|
scopes:
|
944
950
|
global: Küresel kapsamı
|
945
951
|
picker:
|
946
|
-
cancel: İptal
|
952
|
+
cancel: İptal Et
|
947
953
|
change: Seçili kapsamı değiştir
|
948
954
|
choose: seçmek
|
949
955
|
currently_selected: Şu anda seçili kapsam
|
@@ -1012,6 +1018,7 @@ tr:
|
|
1012
1018
|
participatory_space_filters:
|
1013
1019
|
filters:
|
1014
1020
|
areas: alanlar
|
1021
|
+
scope: Kapsam
|
1015
1022
|
select_an_area: Bir bölge seçin
|
1016
1023
|
progress: "Süreç\nİlerleme"
|
1017
1024
|
reference:
|
@@ -1308,6 +1315,8 @@ tr:
|
|
1308
1315
|
x: X
|
1309
1316
|
xing: Xing
|
1310
1317
|
time:
|
1318
|
+
buttons:
|
1319
|
+
select: seçmek
|
1311
1320
|
formats:
|
1312
1321
|
day_of_month: "%b %d"
|
1313
1322
|
day_of_week: "%a"
|
data/config/locales/uk.yml
CHANGED
@@ -37,6 +37,8 @@ uk:
|
|
37
37
|
'false': 'Ні'
|
38
38
|
'true': 'Так'
|
39
39
|
date:
|
40
|
+
buttons:
|
41
|
+
select: Оберіть
|
40
42
|
formats:
|
41
43
|
decidim_short: "%d.%m.%Y"
|
42
44
|
decidim_with_month_name: "%d %B %Y"
|
@@ -191,6 +193,10 @@ uk:
|
|
191
193
|
name: Статистика організації
|
192
194
|
sub_hero:
|
193
195
|
name: Під-багатирський банер
|
196
|
+
core:
|
197
|
+
application_helper:
|
198
|
+
filter_category_values:
|
199
|
+
all: Усі
|
194
200
|
devise:
|
195
201
|
omniauth_registrations:
|
196
202
|
new:
|
@@ -410,7 +416,6 @@ uk:
|
|
410
416
|
scopes:
|
411
417
|
global: Всеохопний обсяг
|
412
418
|
picker:
|
413
|
-
cancel: Скасувати
|
414
419
|
choose: Оберіть
|
415
420
|
title: Оберіть %{field}
|
416
421
|
prompt: Оберіть обсяг
|
@@ -548,6 +553,8 @@ uk:
|
|
548
553
|
weibo: Сіна Вайбо
|
549
554
|
xing: Xing
|
550
555
|
time:
|
556
|
+
buttons:
|
557
|
+
select: Оберіть
|
551
558
|
formats:
|
552
559
|
day_of_month: "%b %d"
|
553
560
|
day_of_week: "%a"
|
data/config/locales/zh-CN.yml
CHANGED
@@ -57,6 +57,8 @@ zh-CN:
|
|
57
57
|
'false': '否'
|
58
58
|
'true': '否'
|
59
59
|
date:
|
60
|
+
buttons:
|
61
|
+
select: 选择
|
60
62
|
formats:
|
61
63
|
decidim_short: "%d/%m/%Y"
|
62
64
|
decidim_with_day_and_month_name: "%A %d %b %Y"
|
@@ -318,6 +320,10 @@ zh-CN:
|
|
318
320
|
name: 组织统计
|
319
321
|
sub_hero:
|
320
322
|
name: 子英雄广告
|
323
|
+
core:
|
324
|
+
application_helper:
|
325
|
+
filter_category_values:
|
326
|
+
all: 所有的
|
321
327
|
devise:
|
322
328
|
omniauth_registrations:
|
323
329
|
new:
|
@@ -906,6 +912,7 @@ zh-CN:
|
|
906
912
|
participatory_space_filters:
|
907
913
|
filters:
|
908
914
|
areas: 地区
|
915
|
+
scope: 范围
|
909
916
|
select_an_area: 选择区域
|
910
917
|
reference:
|
911
918
|
reference: '引用: %{reference}'
|
@@ -1170,6 +1177,8 @@ zh-CN:
|
|
1170
1177
|
whatsapp_web: WhatsApp
|
1171
1178
|
xing: Xing
|
1172
1179
|
time:
|
1180
|
+
buttons:
|
1181
|
+
select: 选择
|
1173
1182
|
formats:
|
1174
1183
|
day_of_month: "%b %d"
|
1175
1184
|
day_of_week: "%a"
|
data/config/locales/zh-TW.yml
CHANGED
@@ -73,6 +73,8 @@ zh-TW:
|
|
73
73
|
'false': '否'
|
74
74
|
'true': '是'
|
75
75
|
date:
|
76
|
+
buttons:
|
77
|
+
select: 選擇
|
76
78
|
formats:
|
77
79
|
decidim_short: "%d/%m/%Y"
|
78
80
|
decidim_short_with_month_name_short: "%d %b %Y"
|
@@ -490,6 +492,9 @@ zh-TW:
|
|
490
492
|
core:
|
491
493
|
actions:
|
492
494
|
unauthorized: 您無權限執行此操作.
|
495
|
+
application_helper:
|
496
|
+
filter_category_values:
|
497
|
+
all: 全部
|
493
498
|
devise:
|
494
499
|
omniauth_registrations:
|
495
500
|
create:
|
@@ -803,7 +808,6 @@ zh-TW:
|
|
803
808
|
title_required: 標題為必填欄位!
|
804
809
|
uploaded: 已上傳
|
805
810
|
validating: 驗證中...
|
806
|
-
validation_error: 驗證錯誤!
|
807
811
|
select_file: 選擇檔案
|
808
812
|
upload_help:
|
809
813
|
dropzone: 將檔案拖曳到此處或點擊按鈕上傳。
|
@@ -1342,6 +1346,7 @@ zh-TW:
|
|
1342
1346
|
participatory_space_filters:
|
1343
1347
|
filters:
|
1344
1348
|
areas: 區域
|
1349
|
+
scope: 範圍
|
1345
1350
|
select_an_area: 選擇一個地區
|
1346
1351
|
public_participation:
|
1347
1352
|
public_participation: 公開展示我的出席情況。
|
@@ -1743,6 +1748,8 @@ zh-TW:
|
|
1743
1748
|
whatsapp_web: WhatsApp
|
1744
1749
|
xing: Xing
|
1745
1750
|
time:
|
1751
|
+
buttons:
|
1752
|
+
select: 選擇
|
1746
1753
|
formats:
|
1747
1754
|
day_of_month: "%b %d"
|
1748
1755
|
day_of_week: "%a"
|
data/config/routes.rb
CHANGED
@@ -137,6 +137,7 @@ Decidim::Core::Engine.routes.draw do
|
|
137
137
|
get "group_members", to: "profiles#group_members", as: "profile_group_members"
|
138
138
|
get "group_admins", to: "profiles#group_admins", as: "profile_group_admins"
|
139
139
|
get "activity", to: "user_activities#index", as: "profile_activity"
|
140
|
+
get "tooltip", to: "profiles#tooltip", as: "profile_tooltip"
|
140
141
|
resources :conversations, except: [:destroy], controller: "user_conversations", as: "profile_conversations"
|
141
142
|
end
|
142
143
|
|
data/decidim-core.gemspec
CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
|
|
21
21
|
}
|
22
22
|
s.summary = "The core of the Decidim framework."
|
23
23
|
s.description = "Adds core features so other engines can hook into the framework."
|
24
|
-
s.license = "AGPL-3.0"
|
24
|
+
s.license = "AGPL-3.0-or-later"
|
25
25
|
s.required_ruby_version = "~> 3.2.0"
|
26
26
|
|
27
27
|
s.files = Dir.chdir(__dir__) do
|
@@ -31,6 +31,9 @@ Gem::Specification.new do |s|
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
+
# Lock Temporarily as it is failing in 0.29 branch. More info: https://github.com/rails/rails/pull/54264
|
35
|
+
s.add_dependency "concurrent-ruby", "= 1.3.4"
|
36
|
+
|
34
37
|
s.add_dependency "active_link_to", "~> 1.0"
|
35
38
|
s.add_dependency "acts_as_list", "~> 1.0"
|
36
39
|
s.add_dependency "batch-loader", "~> 1.2"
|
@@ -27,7 +27,7 @@ module Decidim
|
|
27
27
|
@query = Decidim::Component
|
28
28
|
# remove default ordering if custom order required
|
29
29
|
@query = @query.unscoped if args[:order]
|
30
|
-
@query = @query.where(participatory_space:)
|
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
|
@@ -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
|
-
|
32
|
+
ActiveSupport::TimeWithZone.new(fallback, Time.zone)
|
23
33
|
end
|
24
34
|
end
|
25
35
|
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
|
ActiveModel::Type.register(:integer, Decidim::Attributes::Integer)
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module ContentParsers
|
5
|
+
# Parses any blob URLs from the content and replaces them with references
|
6
|
+
# to those blobs.
|
7
|
+
class BlobParser < BaseParser
|
8
|
+
# Matches all possible URLs pointing to ActiveStorage::Blob objects.
|
9
|
+
#
|
10
|
+
# Possible routes:
|
11
|
+
# get "/blobs/redirect/:signed_id/*filename"
|
12
|
+
# get "/blobs/proxy/:signed_id/*filename"
|
13
|
+
# get "/blobs/:signed_id/*filename"
|
14
|
+
# get "/representations/redirect/:signed_blob_id/:variation_key/*filename"
|
15
|
+
# get "/representations/proxy/:signed_blob_id/:variation_key/*filename"
|
16
|
+
# get "/representations/:signed_blob_id/:variation_key/*filename"
|
17
|
+
# get "/disk/:encoded_key/*filename"
|
18
|
+
#
|
19
|
+
# See:
|
20
|
+
# https://github.com/rails/rails/blob/a7e379896552ce43b822385c03c37f2bd47739d3/activestorage/config/routes.rb#L5-L14
|
21
|
+
BLOB_REGEX = %r{
|
22
|
+
# Group 1: Host part
|
23
|
+
(
|
24
|
+
# Group 2: Domain and subpath part
|
25
|
+
https?://((?!/rails).)+
|
26
|
+
)?
|
27
|
+
/rails/active_storage
|
28
|
+
# Group 3: Blob path, representation path or disk service path
|
29
|
+
/(blobs/redirect|blobs/proxy|blobs|representations/redirect|representations/proxy|representations|disk)
|
30
|
+
# Group 4: Signed ID for blobs or encoded key for disk service
|
31
|
+
/([^/]+)
|
32
|
+
# Group 5: Variation part (only for representations)
|
33
|
+
(
|
34
|
+
# Group 6: Variation key for representations
|
35
|
+
/([\w.=-]+)
|
36
|
+
)?
|
37
|
+
# Group 7: Filename
|
38
|
+
/([\w.=-]+)
|
39
|
+
}x
|
40
|
+
|
41
|
+
def rewrite
|
42
|
+
replace_blobs(content)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def replace_blobs(text)
|
48
|
+
text.gsub(BLOB_REGEX) do |match|
|
49
|
+
type_part = Regexp.last_match(3)
|
50
|
+
key_part = Regexp.last_match(4)
|
51
|
+
|
52
|
+
variation_key = nil
|
53
|
+
blob =
|
54
|
+
if type_part == "disk"
|
55
|
+
# Disk service URL
|
56
|
+
decoded = ActiveStorage.verifier.verified(key_part, purpose: :blob_key)
|
57
|
+
ActiveStorage::Blob.find_by(key: decoded[:key]) if decoded
|
58
|
+
else
|
59
|
+
# Representation or blob
|
60
|
+
if type_part.start_with?("representations")
|
61
|
+
# Representation
|
62
|
+
variation_part = Regexp.last_match(6)
|
63
|
+
variation_key = generate_variation_key(variation_part)
|
64
|
+
end
|
65
|
+
|
66
|
+
ActiveStorage::Blob.find_signed(key_part)
|
67
|
+
end
|
68
|
+
next match unless blob
|
69
|
+
|
70
|
+
"#{blob.to_global_id}#{"/#{variation_key}" if variation_key}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def generate_variation_key(variation_part)
|
75
|
+
# The variation part has to be decoded because it will eventually
|
76
|
+
# expire. This way we can preserve the variation information
|
77
|
+
# longer.
|
78
|
+
variation = ActiveStorage.verifier.verify(variation_part, purpose: :variation)
|
79
|
+
return unless variation
|
80
|
+
|
81
|
+
# Convert to base64 encoded JSON string for better representation within
|
82
|
+
# the URLs. This manually encoded part will not expire as it is
|
83
|
+
# persisted to the database.
|
84
|
+
Base64.strict_encode64(ActiveSupport::JSON.encode(variation))
|
85
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
86
|
+
# This happens if the variation key is already expired in which
|
87
|
+
# case it cannot be represented and instead a URL to the blob is
|
88
|
+
# created.
|
89
|
+
variation_part
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
module Decidim
|
4
4
|
module ContentParsers
|
5
5
|
autoload :BaseParser, "decidim/content_parsers/base_parser"
|
6
|
+
autoload :BlobParser, "decidim/content_parsers/blob_parser"
|
6
7
|
autoload :UserParser, "decidim/content_parsers/user_parser"
|
7
8
|
autoload :UserGroupParser, "decidim/content_parsers/user_group_parser"
|
8
9
|
autoload :HashtagParser, "decidim/content_parsers/hashtag_parser"
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module ContentRenderers
|
5
|
+
# A renderer that searches Global IDs representing blobs in content and
|
6
|
+
# replaces it with a URL to these blobs.
|
7
|
+
#
|
8
|
+
# e.g. gid://<APP_NAME>/ActiveStorage::Blob/1
|
9
|
+
#
|
10
|
+
# OR for representations
|
11
|
+
#
|
12
|
+
# e.g. gid://<APP_NAME>/ActiveStorage::Blob/1/<encoded variant transformations>
|
13
|
+
#
|
14
|
+
# The `<encoded variant transformations>` part of the URL is a Base64
|
15
|
+
# encoded string that contains an unencrypted JSON-encoded value about the
|
16
|
+
# blob transformations. This way the specific representations can be stored
|
17
|
+
# in the database without having these values expiring.
|
18
|
+
#
|
19
|
+
# @see BaseRenderer Examples of how to use a content renderer
|
20
|
+
class BlobRenderer < BaseRenderer
|
21
|
+
# Matches a global id representing a Decidim::User
|
22
|
+
GLOBAL_ID_REGEX = %r{(gid://[\w-]+/ActiveStorage::Blob/\d+)(/([\w=-]+))?}
|
23
|
+
|
24
|
+
# Replaces found Global IDs matching an existing blob with a URL to
|
25
|
+
# that blob. The Global IDs representing an invalid ActiveStorage::Blob
|
26
|
+
# are replaced with an empty string.
|
27
|
+
#
|
28
|
+
# @return [String] the content ready to display (contains HTML)
|
29
|
+
def render(_options = nil)
|
30
|
+
replace_pattern(content, GLOBAL_ID_REGEX)
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def replace_pattern(text, pattern)
|
36
|
+
return text unless text.respond_to?(:gsub)
|
37
|
+
|
38
|
+
text.gsub(pattern) do
|
39
|
+
blob_gid = Regexp.last_match(1)
|
40
|
+
variation_key = Regexp.last_match(3)
|
41
|
+
|
42
|
+
blob = GlobalID::Locator.locate(blob_gid)
|
43
|
+
if variation_key
|
44
|
+
variation = begin
|
45
|
+
ActiveSupport::JSON.decode(Base64.strict_decode64(variation_key))
|
46
|
+
rescue JSON::ParseError
|
47
|
+
variation_key
|
48
|
+
end
|
49
|
+
blob_url(blob, variation)
|
50
|
+
else
|
51
|
+
blob_url(blob)
|
52
|
+
end
|
53
|
+
rescue ActiveRecord::RecordNotFound => _e
|
54
|
+
""
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def blob_url(blob, variation = nil)
|
59
|
+
url = begin
|
60
|
+
if variation
|
61
|
+
blob.variant(variation).url
|
62
|
+
else
|
63
|
+
blob.url
|
64
|
+
end
|
65
|
+
rescue ArgumentError
|
66
|
+
# ArgumentError is raised in case the blob's service is set to
|
67
|
+
# ActiveStorage::Service::DiskService and
|
68
|
+
# `ActiveStorage::Current.url_options` is not set.
|
69
|
+
end
|
70
|
+
raise URI::InvalidURIError if url.blank?
|
71
|
+
|
72
|
+
url
|
73
|
+
rescue URI::InvalidURIError
|
74
|
+
local_blob_url(blob, variation)
|
75
|
+
end
|
76
|
+
|
77
|
+
def local_blob_url(blob, variation = nil)
|
78
|
+
if variation
|
79
|
+
routes.rails_representation_url(blob.variant(variation), only_path: true)
|
80
|
+
else
|
81
|
+
routes.rails_blob_url(blob, only_path: true)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def routes
|
86
|
+
@routes ||= Rails.application.routes.url_helpers
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
module Decidim
|
4
4
|
module ContentRenderers
|
5
5
|
autoload :BaseRenderer, "decidim/content_renderers/base_renderer"
|
6
|
+
autoload :BlobRenderer, "decidim/content_renderers/blob_renderer"
|
6
7
|
autoload :UserRenderer, "decidim/content_renderers/user_renderer"
|
7
8
|
autoload :UserGroupRenderer, "decidim/content_renderers/user_group_renderer"
|
8
9
|
autoload :HashtagRenderer, "decidim/content_renderers/hashtag_renderer"
|
data/lib/decidim/core/engine.rb
CHANGED
@@ -239,6 +239,34 @@ module Decidim
|
|
239
239
|
app.config.action_mailer.deliver_later_queue_name = :mailers
|
240
240
|
end
|
241
241
|
|
242
|
+
initializer "decidim_core.active_storage", before: "active_storage.configs" do |app|
|
243
|
+
next if app.config.active_storage.service_urls_expire_in.present?
|
244
|
+
|
245
|
+
# Ensure that the ActiveStorage URLs are valid long enough because with
|
246
|
+
# the default configuration they would expire in 5 minutes which is a
|
247
|
+
# problem:
|
248
|
+
# a) for the backend blob URL replacement
|
249
|
+
# and
|
250
|
+
# b) for caching
|
251
|
+
#
|
252
|
+
# Note the limitations for each storage service regarding the signed URL
|
253
|
+
# expiration times. This limitation has to be also considered when
|
254
|
+
# defining a caching strategy, otherwise e.g. images or files may not
|
255
|
+
# display correctly when caching is enabled.
|
256
|
+
#
|
257
|
+
# ActiveStorage disk service (default): no limitation
|
258
|
+
#
|
259
|
+
# S3: maximum is 7 days from the creation of the URL
|
260
|
+
# https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html
|
261
|
+
#
|
262
|
+
# Google: maximum is 7 days (604800 seconds)
|
263
|
+
# https://cloud.google.com/storage/docs/access-control/signed-urls
|
264
|
+
#
|
265
|
+
# Azure: no limitation
|
266
|
+
# https://learn.microsoft.com/en-us/azure/storage/common/storage-sas-overview#best-practices-when-using-sas
|
267
|
+
app.config.active_storage.service_urls_expire_in = 7.days
|
268
|
+
end
|
269
|
+
|
242
270
|
initializer "decidim_core.signed_global_id", after: "global_id" do |app|
|
243
271
|
next if app.config.global_id.fetch(:expires_in, nil).present?
|
244
272
|
|
@@ -277,6 +305,12 @@ module Decidim
|
|
277
305
|
app.config.exceptions_app = Decidim::Core::Engine.routes
|
278
306
|
end
|
279
307
|
|
308
|
+
initializer "decidim_core.direct_uploader_paths", after: "decidim_core.exceptions_app" do |_app|
|
309
|
+
config.to_prepare do
|
310
|
+
ActiveStorage::DirectUploadsController.include Decidim::DirectUpload
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
280
314
|
initializer "decidim_core.locales" do |app|
|
281
315
|
app.config.i18n.fallbacks = true
|
282
316
|
end
|
@@ -408,7 +442,7 @@ module Decidim
|
|
408
442
|
|
409
443
|
initializer "decidim_core.content_processors" do |_app|
|
410
444
|
Decidim.configure do |config|
|
411
|
-
config.content_processors += [:user, :user_group, :hashtag, :link]
|
445
|
+
config.content_processors += [:user, :user_group, :hashtag, :link, :blob]
|
412
446
|
end
|
413
447
|
end
|
414
448
|
|
@@ -218,8 +218,15 @@ FactoryBot.define do
|
|
218
218
|
end
|
219
219
|
|
220
220
|
trait :deleted do
|
221
|
+
name { "" }
|
222
|
+
nickname { "" }
|
221
223
|
email { "" }
|
224
|
+
delete_reason { "I want to delete my account" }
|
225
|
+
admin { false }
|
222
226
|
deleted_at { Time.current }
|
227
|
+
avatar { nil }
|
228
|
+
personal_url { "" }
|
229
|
+
about { "" }
|
223
230
|
end
|
224
231
|
|
225
232
|
trait :admin_terms_accepted do
|
@@ -1022,4 +1029,25 @@ FactoryBot.define do
|
|
1022
1029
|
end
|
1023
1030
|
end
|
1024
1031
|
end
|
1032
|
+
|
1033
|
+
factory :blob, class: "ActiveStorage::Blob" do
|
1034
|
+
transient do
|
1035
|
+
filepath { Decidim::Dev.asset("city.jpeg") }
|
1036
|
+
end
|
1037
|
+
|
1038
|
+
filename { File.basename(filepath) }
|
1039
|
+
content_type { MiniMime.lookup_by_filename(filepath)&.content_type || "text/plain" }
|
1040
|
+
|
1041
|
+
before(:create) do |object, evaluator|
|
1042
|
+
object.upload(File.open(evaluator.filepath))
|
1043
|
+
end
|
1044
|
+
|
1045
|
+
trait :image do
|
1046
|
+
filepath { Decidim::Dev.asset("city.jpeg") }
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
trait :document do
|
1050
|
+
filepath { Decidim::Dev.asset("Exampledocument.pdf") }
|
1051
|
+
end
|
1052
|
+
end
|
1025
1053
|
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
|