decidim-core 0.28.3 → 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/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/nav_links/show.erb +3 -3
- data/app/cells/decidim/newsletter_templates/image_text_cta_cell.rb +1 -1
- data/app/cells/decidim/resource_types_filter/show.erb +11 -12
- data/app/cells/decidim/translation_bar/show.erb +3 -3
- 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/menu_helper.rb +1 -1
- data/app/helpers/decidim/sanitize_helper.rb +11 -2
- data/app/helpers/decidim/scopes_helper.rb +5 -2
- data/app/models/decidim/action_log.rb +11 -1
- data/app/models/decidim/attachment.rb +1 -1
- data/app/packs/src/decidim/a11y.js +11 -15
- 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/_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/presenters/decidim/menu_item_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 +5 -5
- data/app/views/decidim/shared/_filters.html.erb +5 -5
- data/app/views/decidim/shared/_orders.html.erb +3 -2
- data/app/views/decidim/shared/filters/_check_boxes_tree.html.erb +1 -1
- data/app/views/decidim/shared/filters/_collection.html.erb +1 -1
- 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/app/views/layouts/decidim/shared/_layout_user_profile.html.erb +2 -2
- data/config/locales/ar.yml +12 -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 +6 -1
- data/config/locales/de.yml +18 -14
- data/config/locales/el.yml +3 -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 +6 -2
- data/config/locales/fr.yml +5 -1
- data/config/locales/ga-IE.yml +5 -0
- data/config/locales/gl.yml +4 -1
- data/config/locales/hu.yml +1 -2
- 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 +18 -16
- data/config/locales/lb.yml +5 -0
- data/config/locales/lt.yml +1 -2
- 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 -2
- data/config/locales/pt-BR.yml +199 -1
- data/config/locales/pt.yml +11 -0
- data/config/locales/ro-RO.yml +302 -180
- data/config/locales/ru.yml +4 -0
- data/config/locales/sk.yml +5 -1
- data/config/locales/sv.yml +452 -81
- 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 -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 +112 -14
- 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
@@ -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:
|
@@ -824,7 +827,6 @@ zh-TW:
|
|
824
827
|
title_required: 標題為必填欄位!
|
825
828
|
uploaded: 已上傳
|
826
829
|
validating: 驗證中...
|
827
|
-
validation_error: 驗證錯誤!
|
828
830
|
select_file: 選擇檔案
|
829
831
|
upload_help:
|
830
832
|
dropzone: 將檔案拖曳到此處或點擊按鈕上傳。
|
@@ -1369,6 +1371,7 @@ zh-TW:
|
|
1369
1371
|
participatory_space_filters:
|
1370
1372
|
filters:
|
1371
1373
|
areas: 區域
|
1374
|
+
scope: 範圍
|
1372
1375
|
select_an_area: 選擇一個地區
|
1373
1376
|
public_participation:
|
1374
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
|
|
@@ -275,6 +303,12 @@ module Decidim
|
|
275
303
|
app.config.exceptions_app = Decidim::Core::Engine.routes
|
276
304
|
end
|
277
305
|
|
306
|
+
initializer "decidim_core.direct_uploader_paths", after: "decidim_core.exceptions_app" do |_app|
|
307
|
+
config.to_prepare do
|
308
|
+
ActiveStorage::DirectUploadsController.include Decidim::DirectUpload
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
278
312
|
initializer "decidim_core.locales" do |app|
|
279
313
|
app.config.i18n.fallbacks = true
|
280
314
|
end
|
@@ -411,7 +445,7 @@ module Decidim
|
|
411
445
|
|
412
446
|
initializer "decidim_core.content_processors" do |_app|
|
413
447
|
Decidim.configure do |config|
|
414
|
-
config.content_processors += [:user, :user_group, :hashtag, :link]
|
448
|
+
config.content_processors += [:user, :user_group, :hashtag, :link, :blob]
|
415
449
|
end
|
416
450
|
end
|
417
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
|
@@ -494,6 +494,16 @@ shared_examples "comments" do
|
|
494
494
|
expect(page).to have_selector("span.comments-count", text: "#{commentable.comments.count} comments")
|
495
495
|
expect(page.find("#add-comment-#{commentable.commentable_type.demodulize}-#{commentable.id}").value).to be_empty
|
496
496
|
end
|
497
|
+
|
498
|
+
it "shows the entry in last activities" do
|
499
|
+
visit decidim.last_activities_path
|
500
|
+
expect(page).to have_content("New comment: #{content}")
|
501
|
+
|
502
|
+
within "#filters" do
|
503
|
+
find("a", class: "filter", text: "Comment", match: :first).click
|
504
|
+
end
|
505
|
+
expect(page).to have_content("New comment: #{content}")
|
506
|
+
end
|
497
507
|
end
|
498
508
|
|
499
509
|
context "when user adds a new comment with a link" do
|
@@ -603,7 +613,7 @@ shared_examples "comments" do
|
|
603
613
|
end
|
604
614
|
|
605
615
|
context "when the user has verified organizations" do
|
606
|
-
let(:user_group) { create(:user_group, :verified) }
|
616
|
+
let(:user_group) { create(:user_group, :verified, organization:) }
|
607
617
|
let(:content) { "This is a new comment" }
|
608
618
|
|
609
619
|
before do
|
@@ -731,6 +741,20 @@ shared_examples "comments" do
|
|
731
741
|
expect(page).to have_content("Edited")
|
732
742
|
end
|
733
743
|
end
|
744
|
+
|
745
|
+
it "has only one edit modal" do
|
746
|
+
expect(page).to have_css("#editCommentModal#{comment.id}", visible: :hidden, count: 1)
|
747
|
+
3.times do |index|
|
748
|
+
sleep 2
|
749
|
+
within "#comment_#{comment.id}" do
|
750
|
+
page.find("[id^='dropdown-trigger']").click
|
751
|
+
click_on "Edit"
|
752
|
+
end
|
753
|
+
fill_in "edit_comment_#{comment.id}", with: " This comment has been edited #{1 + index} times"
|
754
|
+
click_on "Send"
|
755
|
+
end
|
756
|
+
expect(page).to have_css("#editCommentModal#{comment.id}", visible: :all, count: 1)
|
757
|
+
end
|
734
758
|
end
|
735
759
|
end
|
736
760
|
end
|
@@ -1015,7 +1039,6 @@ shared_examples "comments blocked" do
|
|
1015
1039
|
end
|
1016
1040
|
|
1017
1041
|
context "when authenticated" do
|
1018
|
-
let!(:organization) { create(:organization) }
|
1019
1042
|
let!(:user) { create(:user, :confirmed, organization:) }
|
1020
1043
|
let!(:comments) { create_list(:comment, 3, commentable:) }
|
1021
1044
|
|