decidim-core 0.28.4 → 0.28.5
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 +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/translation_bar/show.erb +3 -3
- 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/_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 +12 -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 +16 -12
- data/config/locales/el.yml +3 -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 +5 -1
- data/config/locales/fr.yml +4 -0
- data/config/locales/ga-IE.yml +5 -0
- data/config/locales/gl.yml +4 -0
- data/config/locales/hu.yml +1 -1
- data/config/locales/id-ID.yml +4 -0
- data/config/locales/is-IS.yml +4 -1
- data/config/locales/it.yml +40 -0
- data/config/locales/ja.yml +15 -13
- data/config/locales/lb.yml +5 -0
- data/config/locales/lt.yml +1 -1
- data/config/locales/lv.yml +4 -0
- data/config/locales/nl.yml +6 -1
- data/config/locales/no.yml +5 -0
- data/config/locales/pl.yml +1 -1
- data/config/locales/pt-BR.yml +1 -1
- data/config/locales/pt.yml +11 -0
- data/config/locales/ro-RO.yml +243 -134
- data/config/locales/ru.yml +4 -0
- data/config/locales/sk.yml +5 -1
- data/config/locales/sv.yml +7 -7
- data/config/locales/tr-TR.yml +6 -1
- data/config/locales/uk.yml +4 -1
- data/config/locales/zh-CN.yml +5 -0
- data/config/locales/zh-TW.yml +4 -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
data/config/locales/sk.yml
CHANGED
@@ -342,6 +342,10 @@ sk:
|
|
342
342
|
name: Štatistiky organizácie
|
343
343
|
sub_hero:
|
344
344
|
name: Pruh pod hlavným pruhom
|
345
|
+
core:
|
346
|
+
application_helper:
|
347
|
+
filter_category_values:
|
348
|
+
all: Všetko
|
345
349
|
devise:
|
346
350
|
omniauth_registrations:
|
347
351
|
new:
|
@@ -824,7 +828,7 @@ sk:
|
|
824
828
|
scopes:
|
825
829
|
global: Globální oblast působnosti
|
826
830
|
picker:
|
827
|
-
cancel:
|
831
|
+
cancel: Zrušiť
|
828
832
|
choose: Vybrat
|
829
833
|
title: Vyberte %{field}
|
830
834
|
prompt: Vyberte oblast
|
data/config/locales/sv.yml
CHANGED
@@ -595,7 +595,7 @@ sv:
|
|
595
595
|
already_have_an_account?: Har du redan ett konto?
|
596
596
|
log_in: Logga in
|
597
597
|
newsletter: Få enstaka nyhetsbrev med relevant information
|
598
|
-
newsletter_title:
|
598
|
+
newsletter_title: Tillåtelse att kontakta mig
|
599
599
|
sign_up: Registrera dig
|
600
600
|
subtitle: Registrera dig för att delta i diskussioner och stödja förslag.
|
601
601
|
terms: användarvillkoren
|
@@ -938,9 +938,9 @@ sv:
|
|
938
938
|
index:
|
939
939
|
badge_title: "%{name} märke"
|
940
940
|
how: Hur kan du få det
|
941
|
-
page_description:
|
941
|
+
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.
|
942
942
|
title: Märken
|
943
|
-
description:
|
943
|
+
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.
|
944
944
|
level: Nivå %{level}
|
945
945
|
reached_top: Du har nått den högsta nivån för detta märke.
|
946
946
|
title: Vilka är märkena?
|
@@ -1033,7 +1033,7 @@ sv:
|
|
1033
1033
|
help:
|
1034
1034
|
main_topic:
|
1035
1035
|
default_page:
|
1036
|
-
content: "<p>I %{organization} kan du delta och besluta i de deltagandeprocesser som visas i toppmenyn: processer, samråd,
|
1036
|
+
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"
|
1037
1037
|
title: Vad kan jag göra i %{organization}?
|
1038
1038
|
description: Läs mer om %{organization}.
|
1039
1039
|
title: Allmän hjälp
|
@@ -1436,7 +1436,7 @@ sv:
|
|
1436
1436
|
jump_to: 'Gå till:'
|
1437
1437
|
search: Sök
|
1438
1438
|
state:
|
1439
|
-
active:
|
1439
|
+
active: Pågående
|
1440
1440
|
all: Alla
|
1441
1441
|
future: Framtida
|
1442
1442
|
past: Tidigare
|
@@ -1716,7 +1716,7 @@ sv:
|
|
1716
1716
|
already_signed_out: Du är utloggad.
|
1717
1717
|
new:
|
1718
1718
|
log_in: Logga in
|
1719
|
-
signed_in:
|
1719
|
+
signed_in: Du är inloggad.
|
1720
1720
|
signed_out: Du är utloggad.
|
1721
1721
|
shared:
|
1722
1722
|
links:
|
@@ -1969,7 +1969,7 @@ sv:
|
|
1969
1969
|
account: Konto
|
1970
1970
|
authorizations: Auktoriseringar
|
1971
1971
|
delete_my_account: Radera mitt konto
|
1972
|
-
my_data:
|
1972
|
+
my_data: Min data
|
1973
1973
|
my_interests: Mina intressen
|
1974
1974
|
notifications_settings: Meddelandeinställningar
|
1975
1975
|
profile: Profil
|
data/config/locales/tr-TR.yml
CHANGED
@@ -379,6 +379,10 @@ tr:
|
|
379
379
|
name: Organizasyon istatistikleri
|
380
380
|
sub_hero:
|
381
381
|
name: Alt ana afişi
|
382
|
+
core:
|
383
|
+
application_helper:
|
384
|
+
filter_category_values:
|
385
|
+
all: Herşey
|
382
386
|
devise:
|
383
387
|
omniauth_registrations:
|
384
388
|
new:
|
@@ -957,7 +961,7 @@ tr:
|
|
957
961
|
scopes:
|
958
962
|
global: Küresel kapsamı
|
959
963
|
picker:
|
960
|
-
cancel: İptal
|
964
|
+
cancel: İptal Et
|
961
965
|
change: Seçili kapsamı değiştir
|
962
966
|
choose: seçmek
|
963
967
|
currently_selected: Şu anda seçili kapsam
|
@@ -1027,6 +1031,7 @@ tr:
|
|
1027
1031
|
participatory_space_filters:
|
1028
1032
|
filters:
|
1029
1033
|
areas: alanlar
|
1034
|
+
scope: Kapsam
|
1030
1035
|
select_an_area: Bir bölge seçin
|
1031
1036
|
progress: "Süreç\nİlerleme"
|
1032
1037
|
reference:
|
data/config/locales/uk.yml
CHANGED
@@ -191,6 +191,10 @@ uk:
|
|
191
191
|
name: Статистика організації
|
192
192
|
sub_hero:
|
193
193
|
name: Під-багатирський банер
|
194
|
+
core:
|
195
|
+
application_helper:
|
196
|
+
filter_category_values:
|
197
|
+
all: Усі
|
194
198
|
devise:
|
195
199
|
omniauth_registrations:
|
196
200
|
new:
|
@@ -417,7 +421,6 @@ uk:
|
|
417
421
|
scopes:
|
418
422
|
global: Всеохопний обсяг
|
419
423
|
picker:
|
420
|
-
cancel: Скасувати
|
421
424
|
choose: Оберіть
|
422
425
|
title: Оберіть %{field}
|
423
426
|
prompt: Оберіть обсяг
|
data/config/locales/zh-CN.yml
CHANGED
@@ -324,6 +324,10 @@ zh-CN:
|
|
324
324
|
name: 组织统计
|
325
325
|
sub_hero:
|
326
326
|
name: 子英雄广告
|
327
|
+
core:
|
328
|
+
application_helper:
|
329
|
+
filter_category_values:
|
330
|
+
all: 所有的
|
327
331
|
devise:
|
328
332
|
omniauth_registrations:
|
329
333
|
new:
|
@@ -923,6 +927,7 @@ zh-CN:
|
|
923
927
|
participatory_space_filters:
|
924
928
|
filters:
|
925
929
|
areas: 地区
|
930
|
+
scope: 范围
|
926
931
|
select_an_area: 选择区域
|
927
932
|
reference:
|
928
933
|
reference: '引用: %{reference}'
|
data/config/locales/zh-TW.yml
CHANGED
@@ -500,6 +500,9 @@ zh-TW:
|
|
500
500
|
actions:
|
501
501
|
login_before_access: 請先登入您的帳戶再進行訪問.
|
502
502
|
unauthorized: 您無權限執行此操作.
|
503
|
+
application_helper:
|
504
|
+
filter_category_values:
|
505
|
+
all: 全部
|
503
506
|
devise:
|
504
507
|
omniauth_registrations:
|
505
508
|
create:
|
@@ -1368,6 +1371,7 @@ zh-TW:
|
|
1368
1371
|
participatory_space_filters:
|
1369
1372
|
filters:
|
1370
1373
|
areas: 區域
|
1374
|
+
scope: 範圍
|
1371
1375
|
select_an_area: 選擇一個地區
|
1372
1376
|
public_participation:
|
1373
1377
|
public_participation: 公開展示我的出席情況。
|
data/config/routes.rb
CHANGED
@@ -137,6 +137,7 @@ Decidim::Core::Engine.routes.draw do
|
|
137
137
|
get "group_members", to: "profiles#group_members", as: "profile_group_members"
|
138
138
|
get "group_admins", to: "profiles#group_admins", as: "profile_group_admins"
|
139
139
|
get "activity", to: "user_activities#index", as: "profile_activity"
|
140
|
+
get "tooltip", to: "profiles#tooltip", as: "profile_tooltip"
|
140
141
|
resources :conversations, except: [:destroy], controller: "user_conversations", as: "profile_conversations"
|
141
142
|
end
|
142
143
|
|
data/decidim-core.gemspec
CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
|
|
21
21
|
}
|
22
22
|
s.summary = "The core of the Decidim framework."
|
23
23
|
s.description = "Adds core features so other engines can hook into the framework."
|
24
|
-
s.license = "AGPL-3.0"
|
24
|
+
s.license = "AGPL-3.0-or-later"
|
25
25
|
s.required_ruby_version = "~> 3.1.0"
|
26
26
|
|
27
27
|
s.files = Dir.chdir(__dir__) do
|
@@ -31,6 +31,9 @@ Gem::Specification.new do |s|
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
+
# Lock Temporarily as it is failing in 0.29 branch. More info: https://github.com/rails/rails/pull/54264
|
35
|
+
s.add_dependency "concurrent-ruby", "= 1.2.2"
|
36
|
+
|
34
37
|
s.add_dependency "active_link_to", "~> 1.0"
|
35
38
|
s.add_dependency "acts_as_list", "~> 1.0"
|
36
39
|
s.add_dependency "batch-loader", "~> 1.2"
|
@@ -27,7 +27,7 @@ module Decidim
|
|
27
27
|
@query = Decidim::Component
|
28
28
|
# remove default ordering if custom order required
|
29
29
|
@query = @query.unscoped if args[:order]
|
30
|
-
@query = @query.where(participatory_space:)
|
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
|
# Overrides
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module ContentParsers
|
5
|
+
# Parses any blob URLs from the content and replaces them with references
|
6
|
+
# to those blobs.
|
7
|
+
class BlobParser < BaseParser
|
8
|
+
# Matches all possible URLs pointing to ActiveStorage::Blob objects.
|
9
|
+
#
|
10
|
+
# Possible routes:
|
11
|
+
# get "/blobs/redirect/:signed_id/*filename"
|
12
|
+
# get "/blobs/proxy/:signed_id/*filename"
|
13
|
+
# get "/blobs/:signed_id/*filename"
|
14
|
+
# get "/representations/redirect/:signed_blob_id/:variation_key/*filename"
|
15
|
+
# get "/representations/proxy/:signed_blob_id/:variation_key/*filename"
|
16
|
+
# get "/representations/:signed_blob_id/:variation_key/*filename"
|
17
|
+
# get "/disk/:encoded_key/*filename"
|
18
|
+
#
|
19
|
+
# See:
|
20
|
+
# https://github.com/rails/rails/blob/a7e379896552ce43b822385c03c37f2bd47739d3/activestorage/config/routes.rb#L5-L14
|
21
|
+
BLOB_REGEX = %r{
|
22
|
+
# Group 1: Host part
|
23
|
+
(
|
24
|
+
# Group 2: Domain and subpath part
|
25
|
+
https?://((?!/rails).)+
|
26
|
+
)?
|
27
|
+
/rails/active_storage
|
28
|
+
# Group 3: Blob path, representation path or disk service path
|
29
|
+
/(blobs/redirect|blobs/proxy|blobs|representations/redirect|representations/proxy|representations|disk)
|
30
|
+
# Group 4: Signed ID for blobs or encoded key for disk service
|
31
|
+
/([^/]+)
|
32
|
+
# Group 5: Variation part (only for representations)
|
33
|
+
(
|
34
|
+
# Group 6: Variation key for representations
|
35
|
+
/([\w.=-]+)
|
36
|
+
)?
|
37
|
+
# Group 7: Filename
|
38
|
+
/([\w.=-]+)
|
39
|
+
}x
|
40
|
+
|
41
|
+
def rewrite
|
42
|
+
replace_blobs(content)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def replace_blobs(text)
|
48
|
+
text.gsub(BLOB_REGEX) do |match|
|
49
|
+
type_part = Regexp.last_match(3)
|
50
|
+
key_part = Regexp.last_match(4)
|
51
|
+
|
52
|
+
variation_key = nil
|
53
|
+
blob =
|
54
|
+
if type_part == "disk"
|
55
|
+
# Disk service URL
|
56
|
+
decoded = ActiveStorage.verifier.verified(key_part, purpose: :blob_key)
|
57
|
+
ActiveStorage::Blob.find_by(key: decoded[:key]) if decoded
|
58
|
+
else
|
59
|
+
# Representation or blob
|
60
|
+
if type_part.start_with?("representations")
|
61
|
+
# Representation
|
62
|
+
variation_part = Regexp.last_match(6)
|
63
|
+
variation_key = generate_variation_key(variation_part)
|
64
|
+
end
|
65
|
+
|
66
|
+
ActiveStorage::Blob.find_signed(key_part)
|
67
|
+
end
|
68
|
+
next match unless blob
|
69
|
+
|
70
|
+
"#{blob.to_global_id}#{"/#{variation_key}" if variation_key}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def generate_variation_key(variation_part)
|
75
|
+
# The variation part has to be decoded because it will eventually
|
76
|
+
# expire. This way we can preserve the variation information
|
77
|
+
# longer.
|
78
|
+
variation = ActiveStorage.verifier.verify(variation_part, purpose: :variation)
|
79
|
+
return unless variation
|
80
|
+
|
81
|
+
# Convert to base64 encoded JSON string for better representation within
|
82
|
+
# the URLs. This manually encoded part will not expire as it is
|
83
|
+
# persisted to the database.
|
84
|
+
Base64.strict_encode64(ActiveSupport::JSON.encode(variation))
|
85
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
86
|
+
# This happens if the variation key is already expired in which
|
87
|
+
# case it cannot be represented and instead a URL to the blob is
|
88
|
+
# created.
|
89
|
+
variation_part
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
module Decidim
|
4
4
|
module ContentParsers
|
5
5
|
autoload :BaseParser, "decidim/content_parsers/base_parser"
|
6
|
+
autoload :BlobParser, "decidim/content_parsers/blob_parser"
|
6
7
|
autoload :UserParser, "decidim/content_parsers/user_parser"
|
7
8
|
autoload :UserGroupParser, "decidim/content_parsers/user_group_parser"
|
8
9
|
autoload :HashtagParser, "decidim/content_parsers/hashtag_parser"
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Decidim
|
4
|
+
module ContentRenderers
|
5
|
+
# A renderer that searches Global IDs representing blobs in content and
|
6
|
+
# replaces it with a URL to these blobs.
|
7
|
+
#
|
8
|
+
# e.g. gid://<APP_NAME>/ActiveStorage::Blob/1
|
9
|
+
#
|
10
|
+
# OR for representations
|
11
|
+
#
|
12
|
+
# e.g. gid://<APP_NAME>/ActiveStorage::Blob/1/<encoded variant transformations>
|
13
|
+
#
|
14
|
+
# The `<encoded variant transformations>` part of the URL is a Base64
|
15
|
+
# encoded string that contains an unencrypted JSON-encoded value about the
|
16
|
+
# blob transformations. This way the specific representations can be stored
|
17
|
+
# in the database without having these values expiring.
|
18
|
+
#
|
19
|
+
# @see BaseRenderer Examples of how to use a content renderer
|
20
|
+
class BlobRenderer < BaseRenderer
|
21
|
+
# Matches a global id representing a Decidim::User
|
22
|
+
GLOBAL_ID_REGEX = %r{(gid://[\w-]+/ActiveStorage::Blob/\d+)(/([\w=-]+))?}
|
23
|
+
|
24
|
+
# Replaces found Global IDs matching an existing blob with a URL to
|
25
|
+
# that blob. The Global IDs representing an invalid ActiveStorage::Blob
|
26
|
+
# are replaced with an empty string.
|
27
|
+
#
|
28
|
+
# @return [String] the content ready to display (contains HTML)
|
29
|
+
def render(_options = nil)
|
30
|
+
replace_pattern(content, GLOBAL_ID_REGEX)
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def replace_pattern(text, pattern)
|
36
|
+
return text unless text.respond_to?(:gsub)
|
37
|
+
|
38
|
+
text.gsub(pattern) do
|
39
|
+
blob_gid = Regexp.last_match(1)
|
40
|
+
variation_key = Regexp.last_match(3)
|
41
|
+
|
42
|
+
blob = GlobalID::Locator.locate(blob_gid)
|
43
|
+
if variation_key
|
44
|
+
variation = begin
|
45
|
+
ActiveSupport::JSON.decode(Base64.strict_decode64(variation_key))
|
46
|
+
rescue JSON::ParseError
|
47
|
+
variation_key
|
48
|
+
end
|
49
|
+
blob_url(blob, variation)
|
50
|
+
else
|
51
|
+
blob_url(blob)
|
52
|
+
end
|
53
|
+
rescue ActiveRecord::RecordNotFound => _e
|
54
|
+
""
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def blob_url(blob, variation = nil)
|
59
|
+
url = begin
|
60
|
+
if variation
|
61
|
+
blob.variant(variation).url
|
62
|
+
else
|
63
|
+
blob.url
|
64
|
+
end
|
65
|
+
rescue ArgumentError
|
66
|
+
# ArgumentError is raised in case the blob's service is set to
|
67
|
+
# ActiveStorage::Service::DiskService and
|
68
|
+
# `ActiveStorage::Current.url_options` is not set.
|
69
|
+
end
|
70
|
+
raise URI::InvalidURIError if url.blank?
|
71
|
+
|
72
|
+
url
|
73
|
+
rescue URI::InvalidURIError
|
74
|
+
local_blob_url(blob, variation)
|
75
|
+
end
|
76
|
+
|
77
|
+
def local_blob_url(blob, variation = nil)
|
78
|
+
if variation
|
79
|
+
routes.rails_representation_url(blob.variant(variation), only_path: true)
|
80
|
+
else
|
81
|
+
routes.rails_blob_url(blob, only_path: true)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def routes
|
86
|
+
@routes ||= Rails.application.routes.url_helpers
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
module Decidim
|
4
4
|
module ContentRenderers
|
5
5
|
autoload :BaseRenderer, "decidim/content_renderers/base_renderer"
|
6
|
+
autoload :BlobRenderer, "decidim/content_renderers/blob_renderer"
|
6
7
|
autoload :UserRenderer, "decidim/content_renderers/user_renderer"
|
7
8
|
autoload :UserGroupRenderer, "decidim/content_renderers/user_group_renderer"
|
8
9
|
autoload :HashtagRenderer, "decidim/content_renderers/hashtag_renderer"
|
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
|