decidim-core 0.32.1 → 0.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/cells/decidim/content_blocks/participatory_space_main_data/title.erb +1 -14
- data/app/cells/decidim/following_cell.rb +1 -1
- data/app/cells/decidim/member/show.erb +1 -1
- data/app/cells/decidim/nav_links/show.erb +3 -1
- data/app/cells/decidim/share_text_widget/modal.erb +1 -0
- data/app/cells/decidim/share_widget/modal.erb +1 -0
- data/app/cells/decidim/share_widget/qr_code_modal.erb +2 -4
- data/app/commands/decidim/create_omniauth_registration.rb +17 -2
- data/app/controllers/concerns/decidim/devise_authentication_methods.rb +4 -0
- data/app/controllers/concerns/decidim/devise_controllers.rb +2 -0
- data/app/controllers/concerns/decidim/disable_redirection_to_external_host.rb +2 -0
- data/app/controllers/concerns/decidim/filter_resource.rb +2 -0
- data/app/controllers/concerns/decidim/impersonate_users.rb +2 -2
- data/app/controllers/concerns/decidim/locale_switcher.rb +12 -10
- data/app/controllers/concerns/decidim/needs_permission.rb +22 -10
- data/app/controllers/concerns/decidim/onboarding_action_methods.rb +2 -0
- data/app/controllers/concerns/decidim/paginable.rb +7 -3
- data/app/controllers/concerns/decidim/payload_info.rb +2 -0
- data/app/controllers/concerns/decidim/resource_versions_concern.rb +14 -0
- data/app/controllers/decidim/application_controller.rb +0 -6
- data/app/controllers/decidim/authorization_modals_controller.rb +2 -0
- data/app/controllers/decidim/components/base_controller.rb +2 -0
- data/app/controllers/decidim/devise/passwords_controller.rb +8 -0
- data/app/controllers/decidim/favicon_controller.rb +9 -0
- data/app/controllers/decidim/free_resource_authorization_modals_controller.rb +2 -0
- data/app/controllers/decidim/gamification/badges_controller.rb +2 -0
- data/app/controllers/decidim/homepage_controller.rb +4 -1
- data/app/controllers/decidim/likes_controller.rb +1 -1
- data/app/controllers/decidim/links_controller.rb +2 -0
- data/app/controllers/decidim/manifests_controller.rb +2 -0
- data/app/controllers/decidim/messaging/conversations_controller.rb +10 -4
- data/app/controllers/decidim/newsletters_controller.rb +4 -0
- data/app/controllers/decidim/notifications_subscriptions_controller.rb +4 -0
- data/app/controllers/decidim/offline_controller.rb +5 -1
- data/app/controllers/decidim/open_data_controller.rb +5 -1
- data/app/controllers/decidim/pages_controller.rb +31 -3
- data/app/controllers/decidim/private_downloads_controller.rb +2 -0
- data/app/controllers/decidim/profiles_controller.rb +19 -1
- data/app/controllers/decidim/qr_controller.rb +3 -1
- data/app/controllers/decidim/resource_autocomplete_controller.rb +2 -0
- data/app/controllers/decidim/searches_controller.rb +2 -0
- data/app/controllers/decidim/short_links_controller.rb +4 -0
- data/app/controllers/decidim/static_map_controller.rb +2 -0
- data/app/controllers/decidim/timeouts_controller.rb +4 -0
- data/app/controllers/decidim/tos_controller.rb +2 -0
- data/app/controllers/decidim/upload_validations_controller.rb +4 -2
- data/app/controllers/decidim/user_activities_controller.rb +3 -0
- data/app/forms/decidim/upload_validation_form.rb +16 -10
- data/app/helpers/concerns/decidim/user_role_checker.rb +35 -10
- data/app/helpers/decidim/decidim_form_helper.rb +1 -1
- data/app/helpers/decidim/sanitize_helper.rb +11 -1
- data/app/jobs/decidim/application_job.rb +4 -0
- data/app/models/decidim/authorization.rb +24 -0
- data/app/models/decidim/oauth_application.rb +1 -0
- data/app/models/decidim/user.rb +1 -0
- data/app/models/decidim/user_base_entity.rb +1 -1
- data/app/packs/src/decidim/callout.js +10 -0
- data/app/packs/src/decidim/callout.test.js +32 -0
- data/app/packs/src/decidim/controllers/accordion/accordion.test.js +64 -0
- data/app/packs/src/decidim/controllers/accordion/controller.js +43 -0
- data/app/packs/src/decidim/controllers/autofocus/controller.js +30 -0
- data/app/packs/src/decidim/controllers/language_change/controller.js +21 -8
- data/app/packs/src/decidim/controllers/language_change/language_change.test.js +0 -1
- data/app/packs/src/decidim/controllers/mention/controller.js +32 -3
- data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +8 -5
- data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +1 -1
- data/app/packs/src/decidim/datepicker/datepicker_functions.js +41 -25
- data/app/packs/src/decidim/datepicker/generate_datepicker.js +4 -4
- data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +64 -0
- data/app/packs/src/decidim/datepicker/test/time.test.js +39 -24
- data/app/packs/src/decidim/editor/common/suggestion.js +32 -22
- data/app/packs/src/decidim/editor/extensions/mention/index.js +14 -3
- data/app/packs/src/decidim/editor/index.js +4 -0
- data/app/packs/src/decidim/editor/test/extensions/mention.test.js +32 -9
- data/app/packs/src/decidim/refactor/implementation/autofocus.js +22 -0
- data/app/packs/src/decidim/refactor/implementation/autofocus.test.js +171 -0
- data/app/packs/src/decidim/utilities/dom.js +2 -2
- data/app/packs/stylesheets/decidim/_content_blocks.scss +4 -10
- data/app/packs/stylesheets/decidim/_conversations.scss +0 -8
- data/app/packs/stylesheets/decidim/_modal.scss +16 -8
- data/app/packs/stylesheets/decidim/_modal_share.scss +0 -22
- data/app/packs/stylesheets/decidim/_participatory_spaces.scss +11 -1
- data/app/packs/stylesheets/decidim/_rich_text.scss +12 -9
- data/app/packs/stylesheets/decidim/_typography.scss +5 -2
- data/app/packs/stylesheets/decidim/application.scss +1 -0
- data/app/packs/stylesheets/decidim/legacy/email.scss +0 -5
- data/app/permissions/decidim/permissions.rb +20 -0
- data/app/presenters/decidim/admin_log/component_presenter.rb +1 -1
- data/app/presenters/decidim/log/base_presenter.rb +10 -1
- data/app/presenters/decidim/log/user_presenter.rb +2 -2
- data/app/presenters/decidim/menu_item_presenter.rb +1 -5
- data/app/presenters/decidim/resource_presenter.rb +11 -0
- data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -1
- data/app/serializers/decidim/exporters/open_data_blocked_user_serializer.rb +11 -2
- data/app/serializers/decidim/exporters/open_data_moderation_serializer.rb +25 -1
- data/app/services/decidim/svg_content_validator.rb +554 -0
- data/app/uploaders/decidim/application_uploader.rb +2 -2
- data/app/validators/uploader_image_content_validator.rb +276 -0
- data/app/views/decidim/application/_radio_accordion.html.erb +1 -5
- data/app/views/decidim/messaging/conversations/_add_conversation_users.html.erb +1 -1
- data/app/views/decidim/messaging/conversations/create.js.erb +1 -3
- data/app/views/decidim/shared/_login_modal.html.erb +12 -14
- data/app/views/layouts/decidim/_mailer_logo.html.erb +1 -1
- data/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb +3 -2
- data/config/initializers/rack_attack.rb +10 -0
- data/config/locales/bg.yml +0 -1
- data/config/locales/ca-IT.yml +3 -1
- data/config/locales/ca.yml +10 -6
- data/config/locales/cs.yml +15 -1
- data/config/locales/de.yml +99 -3
- data/config/locales/en.yml +5 -1
- data/config/locales/es-MX.yml +5 -1
- data/config/locales/es-PY.yml +5 -1
- data/config/locales/es.yml +9 -5
- data/config/locales/eu.yml +161 -160
- data/config/locales/fi-plain.yml +3 -1
- data/config/locales/fi.yml +3 -1
- data/config/locales/fr-CA.yml +5 -1
- data/config/locales/fr.yml +5 -1
- data/config/locales/gl.yml +1485 -33
- data/config/locales/hu.yml +0 -1
- data/config/locales/ja.yml +3 -1
- data/config/locales/pl.yml +0 -1
- data/config/locales/pt-BR.yml +0 -1
- data/config/locales/ro-RO.yml +0 -1
- data/config/locales/sk.yml +2 -1
- data/config/locales/sv.yml +68 -12
- data/config/locales/tr-TR.yml +0 -2
- data/config/routes.rb +11 -2
- data/db/migrate/20260909000000_add_failed_attempts_and_locked_at_to_decidim_authorizations.rb +8 -0
- data/decidim-core.gemspec +1 -1
- data/lib/decidim/api/functions/user_entity_list.rb +1 -0
- data/lib/decidim/api/types/user_moderation_type.rb +2 -2
- data/lib/decidim/content_parsers/user_parser.rb +1 -1
- data/lib/decidim/content_parsers.rb +0 -1
- data/lib/decidim/content_renderers/mention_resource_renderer.rb +18 -4
- data/lib/decidim/content_renderers/user_renderer.rb +15 -4
- data/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb +77 -0
- data/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb +58 -0
- data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +23 -1
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/core.rb +9 -0
- data/lib/decidim/form_builder.rb +9 -1
- data/lib/decidim/has_upload_validations.rb +1 -0
- data/lib/decidim/legacy_form_builder.rb +4 -0
- metadata +12 -7
- data/lib/decidim/content_parsers/inline_images_parser.rb +0 -68
- data/lib/tasks/upgrade/decidim_active_storage_migration_tasks.rake +0 -79
|
@@ -15,30 +15,41 @@ module Decidim
|
|
|
15
15
|
before_action :ensure_profile_holder
|
|
16
16
|
before_action :ensure_profile_holder_is_a_user, only: :following
|
|
17
17
|
before_action :ensure_user_not_blocked
|
|
18
|
+
before_action :ensure_user_not_deleted
|
|
18
19
|
|
|
19
20
|
def show
|
|
20
|
-
|
|
21
|
+
enforce_permission_to :read, :profile
|
|
22
|
+
|
|
23
|
+
redirect_to profile_activity_path(nickname: params.expect(:nickname).downcase)
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
def following
|
|
27
|
+
enforce_permission_to :read, :profile
|
|
28
|
+
|
|
24
29
|
@content_cell = "decidim/following"
|
|
25
30
|
@title_key = "following"
|
|
26
31
|
render :show
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def followers
|
|
35
|
+
enforce_permission_to :read, :profile
|
|
36
|
+
|
|
30
37
|
@content_cell = "decidim/followers"
|
|
31
38
|
@title_key = "followers"
|
|
32
39
|
render :show
|
|
33
40
|
end
|
|
34
41
|
|
|
35
42
|
def badges
|
|
43
|
+
enforce_permission_to :read, :profile
|
|
44
|
+
|
|
36
45
|
@content_cell = "decidim/badges"
|
|
37
46
|
@title_key = "badges"
|
|
38
47
|
render :show
|
|
39
48
|
end
|
|
40
49
|
|
|
41
50
|
def activity
|
|
51
|
+
enforce_permission_to :read, :profile
|
|
52
|
+
|
|
42
53
|
@content_cell = "decidim/user_activity"
|
|
43
54
|
@title_key = "activity"
|
|
44
55
|
render :show
|
|
@@ -46,6 +57,13 @@ module Decidim
|
|
|
46
57
|
|
|
47
58
|
private
|
|
48
59
|
|
|
60
|
+
# Deleted users should not normally have a nickname, so in normal situation
|
|
61
|
+
# this should not be possible, but in some edge cases there may be accounts
|
|
62
|
+
# left behind with the username field still available.
|
|
63
|
+
def ensure_user_not_deleted
|
|
64
|
+
raise ActionController::RoutingError, "Deleted User" if profile_holder&.deleted?
|
|
65
|
+
end
|
|
66
|
+
|
|
49
67
|
def ensure_user_not_blocked
|
|
50
68
|
raise ActionController::RoutingError, "Blocked User" if profile_holder&.blocked? && !current_user&.admin?
|
|
51
69
|
end
|
|
@@ -7,7 +7,7 @@ module Decidim
|
|
|
7
7
|
include Decidim::OrganizationHelper
|
|
8
8
|
include Decidim::QrCodeHelper
|
|
9
9
|
|
|
10
|
-
helper_method :resource, :qr_code, :qr_code_image
|
|
10
|
+
helper_method :resource, :qr_code, :qr_code_image, :current_component, :current_participatory_space
|
|
11
11
|
before_action :validate_resource
|
|
12
12
|
before_action :validate_component
|
|
13
13
|
before_action :validate_participatory_space
|
|
@@ -15,6 +15,8 @@ module Decidim
|
|
|
15
15
|
layout false
|
|
16
16
|
|
|
17
17
|
def show
|
|
18
|
+
enforce_permission_to :read, :qr_code
|
|
19
|
+
|
|
18
20
|
respond_to do |format|
|
|
19
21
|
format.html
|
|
20
22
|
format.png { send_data(qr_code.as_png(size: 500), filename: "qr-#{organization_name}-#{parametrized_title}.png") }
|
|
@@ -12,6 +12,8 @@ module Decidim
|
|
|
12
12
|
helper_method :term
|
|
13
13
|
|
|
14
14
|
def index
|
|
15
|
+
enforce_permission_to :read, :search
|
|
16
|
+
|
|
15
17
|
Search.call(term, current_organization, filters, page_params) do
|
|
16
18
|
on(:ok) do |results|
|
|
17
19
|
results_count = results.sum { |results_by_type| results_by_type.last[:count] }
|
|
@@ -8,6 +8,8 @@ module Decidim
|
|
|
8
8
|
# The index action redirects the user to the application root in case
|
|
9
9
|
# someone tries to request the short link URL without an identifier.
|
|
10
10
|
def index
|
|
11
|
+
enforce_permission_to :read, :short_link
|
|
12
|
+
|
|
11
13
|
redirect_to decidim.root_path, status: :moved_permanently
|
|
12
14
|
end
|
|
13
15
|
|
|
@@ -18,6 +20,8 @@ module Decidim
|
|
|
18
20
|
# @raise [ActionController::RoutingError] if a short link does not exist
|
|
19
21
|
# with the given identifier
|
|
20
22
|
def show
|
|
23
|
+
enforce_permission_to :read, :short_link
|
|
24
|
+
|
|
21
25
|
raise ActionController::RoutingError, "Not Found" unless link
|
|
22
26
|
|
|
23
27
|
redirect_to link.target_url, status: :moved_permanently
|
|
@@ -13,6 +13,8 @@ module Decidim
|
|
|
13
13
|
prepend_before_action :skip_timeout, only: :seconds_until_timeout
|
|
14
14
|
|
|
15
15
|
def seconds_until_timeout
|
|
16
|
+
enforce_permission_to :read, :timeout
|
|
17
|
+
|
|
16
18
|
time_remaining = current_user ? Decidim.config.expire_session_after - (Time.current - Time.zone.at(user_session["last_request_at"])) : 0
|
|
17
19
|
respond_to do |format|
|
|
18
20
|
format.json { render json: { seconds_remaining: time_remaining.to_i }, status: :ok }
|
|
@@ -21,6 +23,8 @@ module Decidim
|
|
|
21
23
|
|
|
22
24
|
# If user wants to continue session after inactivity warning.
|
|
23
25
|
def heartbeat
|
|
26
|
+
enforce_permission_to :read, :timeout
|
|
27
|
+
|
|
24
28
|
respond_to do |format|
|
|
25
29
|
format.js
|
|
26
30
|
end
|
|
@@ -7,6 +7,8 @@ module Decidim
|
|
|
7
7
|
skip_before_action :store_current_location
|
|
8
8
|
|
|
9
9
|
def accept_tos
|
|
10
|
+
enforce_permission_to(:update, :user, current_user:)
|
|
11
|
+
|
|
10
12
|
current_user.accepted_tos_version = Time.current
|
|
11
13
|
if current_user.save!
|
|
12
14
|
flash[:notice] = t("accept.success", scope: "decidim.pages.terms_of_service")
|
|
@@ -10,6 +10,8 @@ module Decidim
|
|
|
10
10
|
before_action :underscore_params!
|
|
11
11
|
|
|
12
12
|
def create
|
|
13
|
+
enforce_permission_to :create, :upload_validation
|
|
14
|
+
|
|
13
15
|
@form = form(Decidim::UploadValidationForm).from_params(params)
|
|
14
16
|
|
|
15
17
|
ValidateUpload.call(@form) do
|
|
@@ -23,14 +25,14 @@ module Decidim
|
|
|
23
25
|
end
|
|
24
26
|
end
|
|
25
27
|
|
|
28
|
+
private
|
|
29
|
+
|
|
26
30
|
def organization_time_zone
|
|
27
31
|
return Rails.application.config.time_zone unless current_organization
|
|
28
32
|
|
|
29
33
|
super
|
|
30
34
|
end
|
|
31
35
|
|
|
32
|
-
private
|
|
33
|
-
|
|
34
36
|
def ensure_authenticated!
|
|
35
37
|
return true unless current_organization
|
|
36
38
|
|
|
@@ -12,8 +12,11 @@ module Decidim
|
|
|
12
12
|
helper_method :activities, :resource_types, :user
|
|
13
13
|
|
|
14
14
|
def index
|
|
15
|
+
enforce_permission_to :read, :profile
|
|
16
|
+
|
|
15
17
|
raise ActionController::RoutingError, "Missing user: #{params[:nickname]}" unless user
|
|
16
18
|
raise ActionController::RoutingError, "Blocked User" if user.blocked? && !current_user&.admin?
|
|
19
|
+
raise ActionController::RoutingError, "Deleted User" if user.deleted?
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
private
|
|
@@ -32,16 +32,22 @@ module Decidim
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
34
|
def file_validators
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
if resource_class.constantize == Decidim::ContentBlockAttachment && resource_class.constantize.validators_on(property.to_sym).none?
|
|
36
|
+
extension = blob.filename.to_s.split(".").last&.downcase
|
|
37
|
+
allowed = Decidim::RecordImageUploader.new(nil, :file).extension_allowlist
|
|
38
|
+
errors.add(property.to_sym, I18n.t("errors.messages.allowed_file_content_types", types: allowed.join(", "))) if extension.present? && allowed.exclude?(extension)
|
|
39
|
+
else
|
|
40
|
+
org = organization
|
|
41
|
+
PassthruValidator.new(
|
|
42
|
+
attributes: [property],
|
|
43
|
+
to: resource_class.constantize,
|
|
44
|
+
with: lambda { |record|
|
|
45
|
+
validate_with.tap do |hash|
|
|
46
|
+
hash.merge!(organization: record.try(:organization) || org) if !hash[:organization] && record.respond_to?(:organization=)
|
|
47
|
+
end
|
|
48
|
+
}
|
|
49
|
+
).validate_each(self, property.to_sym, blob)
|
|
50
|
+
end
|
|
45
51
|
end
|
|
46
52
|
|
|
47
53
|
def validate_with
|
|
@@ -10,35 +10,60 @@ module Decidim
|
|
|
10
10
|
def user_has_any_role?(user, participatory_space = nil, broad_check: false)
|
|
11
11
|
return false unless user
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
@user_has_any_role_cache ||= {}
|
|
14
|
+
cache_key = [user.id, participatory_space&.class&.name, participatory_space&.id, broad_check]
|
|
15
|
+
return @user_has_any_role_cache[cache_key] if @user_has_any_role_cache.has_key?(cache_key)
|
|
16
|
+
|
|
17
|
+
@user_has_any_role_cache[cache_key] = global_user_role?(user) || space_user_role?(user, participatory_space, broad_check:)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def global_user_role?(user)
|
|
21
|
+
user.admin || user.roles.any?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def space_user_role?(user, participatory_space, broad_check: false)
|
|
25
|
+
return participatory_process_user_role?(user, participatory_space, broad_check:) if participatory_process_space?(participatory_space)
|
|
26
|
+
return assembly_user_role?(user, participatory_space, broad_check:) if assembly_space?(participatory_space)
|
|
27
|
+
return conference_user_role?(user, participatory_space, broad_check:) if conference_space?(participatory_space)
|
|
28
|
+
return false unless broad_check
|
|
29
|
+
|
|
30
|
+
participatory_process_user_role?(user, nil, broad_check: true) ||
|
|
31
|
+
assembly_user_role?(user, nil, broad_check: true) ||
|
|
32
|
+
conference_user_role?(user, nil, broad_check: true)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def participatory_process_space?(participatory_space)
|
|
36
|
+
Decidim.module_installed?(:participatory_processes) && participatory_space.is_a?(Decidim::ParticipatoryProcess)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def assembly_space?(participatory_space)
|
|
40
|
+
Decidim.module_installed?(:assemblies) && participatory_space.is_a?(Decidim::Assembly)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def conference_space?(participatory_space)
|
|
44
|
+
Decidim.module_installed?(:conferences) && participatory_space.is_a?(Decidim::Conference)
|
|
20
45
|
end
|
|
21
46
|
|
|
22
47
|
def participatory_process_user_role?(user, participatory_process = nil, broad_check: false)
|
|
23
48
|
return false unless Decidim.module_installed?(:participatory_processes)
|
|
49
|
+
return false unless participatory_process.nil? || participatory_process.is_a?(Decidim::ParticipatoryProcess)
|
|
24
50
|
return Decidim::ParticipatoryProcessUserRole.exists?(user:) if broad_check
|
|
25
|
-
return false unless participatory_process.is_a?(Decidim::ParticipatoryProcess)
|
|
26
51
|
|
|
27
52
|
Decidim::ParticipatoryProcessUserRole.exists?(user:, participatory_process:)
|
|
28
53
|
end
|
|
29
54
|
|
|
30
55
|
def assembly_user_role?(user, assembly = nil, broad_check: false)
|
|
31
56
|
return false unless Decidim.module_installed?(:assemblies)
|
|
57
|
+
return false unless assembly.nil? || assembly.is_a?(Decidim::Assembly)
|
|
32
58
|
return Decidim::AssemblyUserRole.exists?(user:) if broad_check
|
|
33
|
-
return false unless assembly.is_a?(Decidim::Assembly)
|
|
34
59
|
|
|
35
60
|
Decidim::AssemblyUserRole.exists?(user:, assembly:)
|
|
36
61
|
end
|
|
37
62
|
|
|
38
63
|
def conference_user_role?(user, conference = nil, broad_check: false)
|
|
39
64
|
return false unless Decidim.module_installed?(:conferences)
|
|
65
|
+
return false unless conference.nil? || conference.is_a?(Decidim::Conference)
|
|
40
66
|
return Decidim::ConferenceUserRole.exists?(user:) if broad_check
|
|
41
|
-
return false unless conference.is_a?(Decidim::Conference)
|
|
42
67
|
|
|
43
68
|
Decidim::ConferenceUserRole.exists?(user:, conference:)
|
|
44
69
|
end
|
|
@@ -194,7 +194,7 @@ module Decidim
|
|
|
194
194
|
options[:data] ||= {}
|
|
195
195
|
options[:data].update(live_validate: true, validate_on_blur: true)
|
|
196
196
|
options[:data].update(controller: "") unless options[:data].has_key?(:controller)
|
|
197
|
-
options[:data][:controller] += " form-validator"
|
|
197
|
+
options[:data][:controller] += " form-validator autofocus"
|
|
198
198
|
options[:data][:controller].strip!
|
|
199
199
|
|
|
200
200
|
options[:html] ||= {}
|
|
@@ -55,8 +55,18 @@ module Decidim
|
|
|
55
55
|
)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# Public: Escapes the HTML special characters of a user-inputted string.
|
|
59
|
+
#
|
|
60
|
+
# The result is marked as HTML-safe, so that when the views output it with
|
|
61
|
+
# `<%= %>` (or pass it to a tag helper) Rails does not escape it a second
|
|
62
|
+
# time. Otherwise, a text such as `O'Neil` would end up as `O&#39;Neil` in
|
|
63
|
+
# the HTML and the browser would display the `'` entity literally.
|
|
64
|
+
#
|
|
65
|
+
# text - A String (or an object responding to `to_str`).
|
|
66
|
+
#
|
|
67
|
+
# Returns an HTML-safe String.
|
|
58
68
|
def decidim_html_escape(text)
|
|
59
|
-
ERB::Util.
|
|
69
|
+
ERB::Util.html_escape(text.to_str)
|
|
60
70
|
end
|
|
61
71
|
|
|
62
72
|
def decidim_url_escape(text)
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Decidim
|
|
4
4
|
class ApplicationJob < ActiveJob::Base
|
|
5
|
+
# Wait until the commits on the transaction are actually on the database
|
|
6
|
+
# before running the jobs, preventing possible race conditions.
|
|
7
|
+
self.enqueue_after_transaction_commit = true
|
|
8
|
+
|
|
5
9
|
# Automatically retry jobs that encountered a deadlock
|
|
6
10
|
retry_on ActiveRecord::Deadlocked
|
|
7
11
|
|
|
@@ -98,6 +98,30 @@ module Decidim
|
|
|
98
98
|
user == self.user
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
def record_failed_attempt!
|
|
102
|
+
with_lock do
|
|
103
|
+
increment!(:failed_attempts) # rubocop:disable Rails/SkipsModelValidations
|
|
104
|
+
return unless failed_attempts >= Decidim.verification_max_failed_attempts
|
|
105
|
+
|
|
106
|
+
update!(locked_at: Time.current)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def reset_failed_attempts!
|
|
111
|
+
update!(failed_attempts: 0, locked_at: nil)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def locked_for_confirmation?
|
|
115
|
+
locked_at.present? && locked_at + Decidim.verification_unlock_in > Time.current
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def clear_expired_lock!
|
|
119
|
+
return if locked_at.blank?
|
|
120
|
+
return if locked_for_confirmation?
|
|
121
|
+
|
|
122
|
+
update!(locked_at: nil, failed_attempts: 0)
|
|
123
|
+
end
|
|
124
|
+
|
|
101
125
|
private
|
|
102
126
|
|
|
103
127
|
def active_handler?
|
|
@@ -18,6 +18,7 @@ module Decidim
|
|
|
18
18
|
:organization_logo,
|
|
19
19
|
file_size: { less_than_or_equal_to: ->(record) { record.maximum_upload_size } },
|
|
20
20
|
uploader_content_type: true,
|
|
21
|
+
uploader_image_content: true,
|
|
21
22
|
uploader_image_dimensions: true
|
|
22
23
|
)
|
|
23
24
|
attached_config[:organization_logo] = OpenStruct.new(uploader: OAuthApplicationLogoUploader)
|
data/app/models/decidim/user.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Decidim
|
|
|
32
32
|
has_many :access_tokens, class_name: "Doorkeeper::AccessToken", foreign_key: :resource_owner_id, dependent: :destroy
|
|
33
33
|
has_many :reminders, foreign_key: "decidim_user_id", class_name: "Decidim::Reminder", dependent: :destroy
|
|
34
34
|
has_many :private_exports, class_name: "Decidim::PrivateExport", dependent: :destroy, inverse_of: :attached_to, as: :attached_to
|
|
35
|
+
has_many :impersonation_logs, foreign_key: "decidim_user_id", class_name: "Decidim::ImpersonationLog", dependent: :destroy
|
|
35
36
|
|
|
36
37
|
validates :name, presence: true, unless: -> { deleted? }
|
|
37
38
|
validates :nickname,
|
|
@@ -62,7 +62,7 @@ module Decidim
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def users_followings
|
|
65
|
-
@users_followings ||= Decidim::UserBaseEntity.joins(:follows).where(decidim_follows: { user: self })
|
|
65
|
+
@users_followings ||= Decidim::UserBaseEntity.joins(:follows).where(decidim_follows: { user: self }).order(decidim_follows: { created_at: :asc, id: :asc })
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def followings_blocked?
|
|
@@ -19,3 +19,13 @@ document.addEventListener("turbo:load", () => {
|
|
|
19
19
|
callout.innerHTML += " ";
|
|
20
20
|
}, 500);
|
|
21
21
|
});
|
|
22
|
+
|
|
23
|
+
document.addEventListener("click", (event) => {
|
|
24
|
+
const closeButton = event.target.closest("[data-alert-box] [data-close]");
|
|
25
|
+
|
|
26
|
+
if (!closeButton) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
closeButton.closest("[data-alert-box]").remove();
|
|
31
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import "src/decidim/callout"
|
|
2
|
+
|
|
3
|
+
describe("callout close button", () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
document.body.innerHTML = `
|
|
6
|
+
<div id="flash-messages-container">
|
|
7
|
+
<div class="flash success" data-alert-box role="alert" aria-atomic="true">
|
|
8
|
+
<div class="flash__message">Signed in successfully.</div>
|
|
9
|
+
<button type="button" class="close-button" data-close aria-label="Dismiss">×</button>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
`;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("removes the alert box when the close button is clicked", () => {
|
|
16
|
+
const closeButton = document.querySelector("[data-close]");
|
|
17
|
+
|
|
18
|
+
expect(document.querySelector("[data-alert-box]")).not.toBeNull();
|
|
19
|
+
|
|
20
|
+
closeButton.click();
|
|
21
|
+
|
|
22
|
+
expect(document.querySelector("[data-alert-box]")).toBeNull();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("does nothing when clicking outside of the close button", () => {
|
|
26
|
+
const message = document.querySelector(".flash__message");
|
|
27
|
+
|
|
28
|
+
message.click();
|
|
29
|
+
|
|
30
|
+
expect(document.querySelector("[data-alert-box]")).not.toBeNull();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint max-lines: ["error", 350] */
|
|
1
2
|
/* global jest */
|
|
2
3
|
|
|
3
4
|
import AccordionController from "src/decidim/controllers/accordion/controller";
|
|
@@ -99,6 +100,69 @@ describe("AccordionController", () => {
|
|
|
99
100
|
});
|
|
100
101
|
});
|
|
101
102
|
|
|
103
|
+
describe("_setupInertPanels", () => {
|
|
104
|
+
const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
panel1.setAttribute("inert", "");
|
|
108
|
+
panel1.setAttribute("aria-hidden", "true");
|
|
109
|
+
panel1.setAttribute("tabindex", "-1");
|
|
110
|
+
panel2.setAttribute("aria-hidden", "true");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("only manages panels with the inert attribute", async () => {
|
|
114
|
+
controller._setupInertPanels();
|
|
115
|
+
expect(controller._panelObservers.length).toBe(1);
|
|
116
|
+
|
|
117
|
+
panel2.setAttribute("aria-hidden", "false");
|
|
118
|
+
await nextTick();
|
|
119
|
+
expect(panel2.hasAttribute("inert")).toBe(false);
|
|
120
|
+
expect("inertManaged" in panel2.dataset).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("removes inert and moves focus to the panel when it gets expanded", async () => {
|
|
124
|
+
controller._setupInertPanels();
|
|
125
|
+
|
|
126
|
+
panel1.setAttribute("aria-hidden", "false");
|
|
127
|
+
await nextTick();
|
|
128
|
+
expect(panel1.hasAttribute("inert")).toBe(false);
|
|
129
|
+
expect(document.activeElement).toBe(panel1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("restores inert when the panel gets collapsed again", async () => {
|
|
133
|
+
controller._setupInertPanels();
|
|
134
|
+
|
|
135
|
+
panel1.setAttribute("aria-hidden", "false");
|
|
136
|
+
await nextTick();
|
|
137
|
+
panel1.setAttribute("aria-hidden", "true");
|
|
138
|
+
await nextTick();
|
|
139
|
+
expect(panel1.hasAttribute("inert")).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("keeps managing an expanded panel across teardown and setup", async () => {
|
|
143
|
+
controller._setupInertPanels();
|
|
144
|
+
|
|
145
|
+
panel1.setAttribute("aria-hidden", "false");
|
|
146
|
+
await nextTick();
|
|
147
|
+
controller._teardownInertPanels();
|
|
148
|
+
|
|
149
|
+
panel1.setAttribute("aria-hidden", "true");
|
|
150
|
+
controller._setupInertPanels();
|
|
151
|
+
expect(controller._panelObservers.length).toBe(1);
|
|
152
|
+
expect(panel1.hasAttribute("inert")).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("stops syncing after teardown", async () => {
|
|
156
|
+
controller._setupInertPanels();
|
|
157
|
+
controller._teardownInertPanels();
|
|
158
|
+
expect(controller._panelObservers.length).toBe(0);
|
|
159
|
+
|
|
160
|
+
panel1.setAttribute("aria-hidden", "false");
|
|
161
|
+
await nextTick();
|
|
162
|
+
expect(panel1.hasAttribute("inert")).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
102
166
|
describe("_applyBreakpointOpenAttributes", () => {
|
|
103
167
|
beforeEach(() => {
|
|
104
168
|
controller.initialize();
|
|
@@ -31,6 +31,8 @@ export default class extends Controller {
|
|
|
31
31
|
|
|
32
32
|
this.fixPanelRole();
|
|
33
33
|
|
|
34
|
+
this._setupInertPanels();
|
|
35
|
+
|
|
34
36
|
this.expandIfNeeded();
|
|
35
37
|
|
|
36
38
|
this.boundReconnect = this.reconnect.bind(this);
|
|
@@ -52,6 +54,8 @@ export default class extends Controller {
|
|
|
52
54
|
this.toggleButton.removeEventListener("click", this.boundExpand);
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
this._teardownInertPanels();
|
|
58
|
+
|
|
55
59
|
this._stopListeningForBreakpointChanges();
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -166,6 +170,45 @@ export default class extends Controller {
|
|
|
166
170
|
this.previouslyExpanded = this.toggleButton.getAttribute("aria-expanded");
|
|
167
171
|
}
|
|
168
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Syncs the `inert` attribute of partially visible panels (those rendered
|
|
175
|
+
* inert) with their `aria-hidden` state, moving focus to the panel on expand.
|
|
176
|
+
*
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
179
|
+
_setupInertPanels() {
|
|
180
|
+
this._panelObservers = [];
|
|
181
|
+
|
|
182
|
+
this.element.querySelectorAll("[data-controls]").forEach((trigger) => {
|
|
183
|
+
const panel = document.getElementById(trigger.dataset.controls);
|
|
184
|
+
if (!panel || (!panel.hasAttribute("inert") && !("inertManaged" in panel.dataset))) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
panel.dataset.inertManaged = "";
|
|
189
|
+
panel.toggleAttribute("inert", panel.getAttribute("aria-hidden") === "true");
|
|
190
|
+
|
|
191
|
+
const observer = new MutationObserver(() => {
|
|
192
|
+
if (panel.getAttribute("aria-hidden") === "true") {
|
|
193
|
+
panel.setAttribute("inert", "");
|
|
194
|
+
} else {
|
|
195
|
+
panel.removeAttribute("inert");
|
|
196
|
+
panel.focus();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
observer.observe(panel, { attributes: true, attributeFilter: ["aria-hidden"] });
|
|
200
|
+
|
|
201
|
+
this._panelObservers.push(observer);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
_teardownInertPanels() {
|
|
206
|
+
if (this._panelObservers) {
|
|
207
|
+
this._panelObservers.forEach((observer) => observer.disconnect());
|
|
208
|
+
this._panelObservers = [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
169
212
|
fixPanelRole() {
|
|
170
213
|
const panelRole = this.element.dataset.panelRole;
|
|
171
214
|
if (!panelRole) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import autofocus from "src/decidim/refactor/implementation/autofocus"
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
connect() {
|
|
6
|
+
this.handleTabsChange = this.handleTabsChange.bind(this)
|
|
7
|
+
|
|
8
|
+
// Foundation tabs (used for translated fields when locales <= 4)
|
|
9
|
+
this.tabs = Array.from(this.element.querySelectorAll("ul[data-tabs]"))
|
|
10
|
+
if (typeof $ === "function" && this.tabs.length > 0) {
|
|
11
|
+
this.tabs.forEach((tabs) => $(tabs).on("change.zf.tabs", this.handleTabsChange))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Language selector dropdown (used for translated fields when locales > 4)
|
|
15
|
+
if (this.element.querySelector("select.language-change")) {
|
|
16
|
+
autofocus(this.element)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
disconnect() {
|
|
21
|
+
if (typeof $ === "function" && this.tabs && this.tabs.length > 0) {
|
|
22
|
+
this.tabs.forEach((tabs) => $(tabs).off("change.zf.tabs", this.handleTabsChange))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
handleTabsChange(event) {
|
|
27
|
+
const activePane = $(event.target).parent().next(".tabs-content").find(".tabs-panel.is-active")[0]
|
|
28
|
+
autofocus(activePane)
|
|
29
|
+
}
|
|
30
|
+
}
|