decidim-core 0.29.1 → 0.29.2
Sign up to get free protection for your applications and to get access to all the features.
- 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/diff_cell.rb +4 -0
- 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/destroy_account.rb +3 -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/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/sanitize_helper.rb +11 -2
- data/app/models/decidim/attachment.rb +1 -1
- 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 +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/_labels.scss +1 -1
- 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/services/decidim/base_diff_renderer.rb +26 -2
- data/app/services/decidim/email_notification_generator.rb +14 -5
- data/app/views/decidim/pages/_tabbed.html.erb +2 -2
- data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
- data/config/locales/ar.yml +16 -0
- data/config/locales/bn-BD.yml +1 -0
- data/config/locales/bs-BA.yml +98 -0
- data/config/locales/ca.yml +13 -9
- data/config/locales/cs.yml +5 -0
- data/config/locales/de.yml +18 -14
- data/config/locales/el.yml +7 -0
- data/config/locales/en.yml +4 -0
- data/config/locales/es-MX.yml +5 -1
- data/config/locales/es-PY.yml +5 -1
- data/config/locales/es.yml +11 -7
- data/config/locales/eu.yml +198 -181
- data/config/locales/fi-plain.yml +4 -0
- data/config/locales/fi.yml +39 -35
- data/config/locales/fr-CA.yml +6 -2
- data/config/locales/fr.yml +5 -1
- data/config/locales/ga-IE.yml +9 -0
- data/config/locales/gl.yml +8 -0
- data/config/locales/hu.yml +3 -3
- 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 +15 -13
- data/config/locales/lb.yml +9 -0
- data/config/locales/lt.yml +5 -1
- 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 -1
- data/config/locales/pt-BR.yml +2 -1
- data/config/locales/pt.yml +14 -0
- data/config/locales/ro-RO.yml +258 -135
- data/config/locales/ru.yml +8 -0
- data/config/locales/sk.yml +9 -1
- data/config/locales/sv.yml +7 -7
- 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 -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 +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 +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/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/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/tasks/upgrade/decidim_fix_categorization.rake +34 -8
- metadata +28 -7
data/config/locales/ru.yml
CHANGED
@@ -51,6 +51,8 @@ ru:
|
|
51
51
|
'false': 'Нет'
|
52
52
|
'true': 'Да'
|
53
53
|
date:
|
54
|
+
buttons:
|
55
|
+
select: Выбрать
|
54
56
|
formats:
|
55
57
|
decidim_short: "%d.%m.%Y"
|
56
58
|
decidim_with_month_name: "%d %B %Y"
|
@@ -246,6 +248,10 @@ ru:
|
|
246
248
|
name: Статистика организации
|
247
249
|
sub_hero:
|
248
250
|
name: Под-богатырский баннер
|
251
|
+
core:
|
252
|
+
application_helper:
|
253
|
+
filter_category_values:
|
254
|
+
all: Все
|
249
255
|
devise:
|
250
256
|
omniauth_registrations:
|
251
257
|
new:
|
@@ -709,6 +715,8 @@ ru:
|
|
709
715
|
weibo: Сина Вейбо
|
710
716
|
xing: Xing
|
711
717
|
time:
|
718
|
+
buttons:
|
719
|
+
select: Выбрать
|
712
720
|
formats:
|
713
721
|
day_of_month: "%b %d"
|
714
722
|
day_of_week: "%a"
|
data/config/locales/sk.yml
CHANGED
@@ -67,6 +67,8 @@ sk:
|
|
67
67
|
'false': 'Nie'
|
68
68
|
'true': 'Áno'
|
69
69
|
date:
|
70
|
+
buttons:
|
71
|
+
select: Vybrat
|
70
72
|
formats:
|
71
73
|
decidim_short: "%d/%m/%Y"
|
72
74
|
decidim_short_with_month_name_short: "%d %b %Y"
|
@@ -336,6 +338,10 @@ sk:
|
|
336
338
|
name: Štatistiky organizácie
|
337
339
|
sub_hero:
|
338
340
|
name: Pruh pod hlavným pruhom
|
341
|
+
core:
|
342
|
+
application_helper:
|
343
|
+
filter_category_values:
|
344
|
+
all: Všetko
|
339
345
|
devise:
|
340
346
|
omniauth_registrations:
|
341
347
|
new:
|
@@ -810,7 +816,7 @@ sk:
|
|
810
816
|
scopes:
|
811
817
|
global: Globální oblast působnosti
|
812
818
|
picker:
|
813
|
-
cancel:
|
819
|
+
cancel: Zrušiť
|
814
820
|
choose: Vybrat
|
815
821
|
title: Vyberte %{field}
|
816
822
|
prompt: Vyberte oblast
|
@@ -1081,6 +1087,8 @@ sk:
|
|
1081
1087
|
whatsapp_web: WhatsApp
|
1082
1088
|
xing: Xing
|
1083
1089
|
time:
|
1090
|
+
buttons:
|
1091
|
+
select: Vybrat
|
1084
1092
|
formats:
|
1085
1093
|
day_of_month: "%b %d"
|
1086
1094
|
day_of_week: "%a"
|
data/config/locales/sv.yml
CHANGED
@@ -594,7 +594,7 @@ sv:
|
|
594
594
|
already_have_an_account?: Har du redan ett konto?
|
595
595
|
log_in: Logga in
|
596
596
|
newsletter: Få enstaka nyhetsbrev med relevant information
|
597
|
-
newsletter_title:
|
597
|
+
newsletter_title: Tillåtelse att kontakta mig
|
598
598
|
sign_up: Skapa ett konto
|
599
599
|
subtitle: Skapa ett konto för att delta i plattformen.
|
600
600
|
terms: användarvillkoren
|
@@ -947,9 +947,9 @@ sv:
|
|
947
947
|
index:
|
948
948
|
badge_title: "%{name} märke"
|
949
949
|
how: Hur kan du få det
|
950
|
-
page_description:
|
950
|
+
page_description: Märkena visar att du varit en aktiv deltagare på plattformen. Genom att utforska, delta och samverka på plattformen får du olika märken. Här är alla märken och hur du kan få dem.
|
951
951
|
title: Märken
|
952
|
-
description:
|
952
|
+
description: Märkena visar att du varit en aktiv deltagare på plattformen. Genom att utforska, delta och samverka på plattformen får du olika märken.
|
953
953
|
level: Nivå %{level}
|
954
954
|
reached_top: Du har nått den högsta nivån för detta märke.
|
955
955
|
title: Vilka är märkena?
|
@@ -1042,7 +1042,7 @@ sv:
|
|
1042
1042
|
help:
|
1043
1043
|
main_topic:
|
1044
1044
|
default_page:
|
1045
|
-
content: "<p>I %{organization} kan du delta och besluta i de deltagandeprocesser som visas i toppmenyn: processer, samråd,
|
1045
|
+
content: "<p>I %{organization} kan du delta och besluta i de deltagandeprocesser som visas i toppmenyn: processer, samråd, initiativ och konsultationer.</p> <p>Inom varje område kan du delta på olika sätt: lämna förslag – enskilt eller med andra människor – delta i debatter, prioritera projekt att genomföra, delta i möten med mera.</p>\n"
|
1046
1046
|
title: Vad kan jag göra i %{organization}?
|
1047
1047
|
description: Läs mer om %{organization}.
|
1048
1048
|
title: Allmänt
|
@@ -1444,7 +1444,7 @@ sv:
|
|
1444
1444
|
jump_to: 'Gå till:'
|
1445
1445
|
search: Sök
|
1446
1446
|
state:
|
1447
|
-
active:
|
1447
|
+
active: Pågående
|
1448
1448
|
all: Alla
|
1449
1449
|
future: Framtida
|
1450
1450
|
past: Tidigare
|
@@ -1724,7 +1724,7 @@ sv:
|
|
1724
1724
|
already_signed_out: Du är utloggad.
|
1725
1725
|
new:
|
1726
1726
|
log_in: Logga in
|
1727
|
-
signed_in:
|
1727
|
+
signed_in: Du är inloggad.
|
1728
1728
|
signed_out: Du är utloggad.
|
1729
1729
|
shared:
|
1730
1730
|
links:
|
@@ -1975,7 +1975,7 @@ sv:
|
|
1975
1975
|
account: Konto
|
1976
1976
|
authorizations: Auktoriseringar
|
1977
1977
|
delete_my_account: Radera mitt konto
|
1978
|
-
my_data:
|
1978
|
+
my_data: Min data
|
1979
1979
|
my_interests: Mina intressen
|
1980
1980
|
notifications_settings: Meddelandeinställningar
|
1981
1981
|
profile: Profil
|
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:
|
@@ -1341,6 +1346,7 @@ zh-TW:
|
|
1341
1346
|
participatory_space_filters:
|
1342
1347
|
filters:
|
1343
1348
|
areas: 區域
|
1349
|
+
scope: 範圍
|
1344
1350
|
select_an_area: 選擇一個地區
|
1345
1351
|
public_participation:
|
1346
1352
|
public_participation: 公開展示我的出席情況。
|
@@ -1742,6 +1748,8 @@ zh-TW:
|
|
1742
1748
|
whatsapp_web: WhatsApp
|
1743
1749
|
xing: Xing
|
1744
1750
|
time:
|
1751
|
+
buttons:
|
1752
|
+
select: 選擇
|
1745
1753
|
formats:
|
1746
1754
|
day_of_month: "%b %d"
|
1747
1755
|
day_of_week: "%a"
|
data/config/routes.rb
CHANGED
@@ -137,6 +137,7 @@ Decidim::Core::Engine.routes.draw do
|
|
137
137
|
get "group_members", to: "profiles#group_members", as: "profile_group_members"
|
138
138
|
get "group_admins", to: "profiles#group_admins", as: "profile_group_admins"
|
139
139
|
get "activity", to: "user_activities#index", as: "profile_activity"
|
140
|
+
get "tooltip", to: "profiles#tooltip", as: "profile_tooltip"
|
140
141
|
resources :conversations, except: [:destroy], controller: "user_conversations", as: "profile_conversations"
|
141
142
|
end
|
142
143
|
|
data/decidim-core.gemspec
CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
|
|
21
21
|
}
|
22
22
|
s.summary = "The core of the Decidim framework."
|
23
23
|
s.description = "Adds core features so other engines can hook into the framework."
|
24
|
-
s.license = "AGPL-3.0"
|
24
|
+
s.license = "AGPL-3.0-or-later"
|
25
25
|
s.required_ruby_version = "~> 3.2.0"
|
26
26
|
|
27
27
|
s.files = Dir.chdir(__dir__) do
|
@@ -31,6 +31,9 @@ Gem::Specification.new do |s|
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
+
# Lock Temporarily as it is failing in 0.29 branch. More info: https://github.com/rails/rails/pull/54264
|
35
|
+
s.add_dependency "concurrent-ruby", "= 1.3.4"
|
36
|
+
|
34
37
|
s.add_dependency "active_link_to", "~> 1.0"
|
35
38
|
s.add_dependency "acts_as_list", "~> 1.0"
|
36
39
|
s.add_dependency "batch-loader", "~> 1.2"
|
@@ -27,7 +27,7 @@ module Decidim
|
|
27
27
|
@query = Decidim::Component
|
28
28
|
# remove default ordering if custom order required
|
29
29
|
@query = @query.unscoped if args[:order]
|
30
|
-
@query = @query.where(participatory_space:)
|
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"
|