decidim-core 0.25.0 → 0.26.0.rc2
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/activity_cell.rb +2 -1
- data/app/cells/decidim/author/flag_user.erb +1 -1
- data/app/cells/decidim/author/profile_inline.erb +1 -1
- data/app/cells/decidim/author/withdraw.erb +2 -2
- data/app/cells/decidim/author_cell.rb +32 -0
- data/app/cells/decidim/card_m_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/cta_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/hero_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/highlighted_content_banner/show.erb +1 -1
- data/app/cells/decidim/content_blocks/how_to_participate/show.erb +1 -1
- data/app/cells/decidim/content_blocks/last_activity_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/stats_cell.rb +12 -0
- data/app/cells/decidim/endorsers_list_cell.rb +3 -1
- data/app/cells/decidim/flag_modal/flag_user.erb +2 -2
- data/app/cells/decidim/flag_modal/show.erb +2 -2
- data/app/cells/decidim/flag_modal_cell.rb +10 -0
- data/app/cells/decidim/following/show.erb +17 -8
- data/app/cells/decidim/following_cell.rb +6 -2
- data/app/cells/decidim/notification/show.erb +31 -0
- data/app/cells/decidim/notification_cell.rb +20 -0
- data/app/cells/decidim/notifications/show.erb +1 -24
- data/app/cells/decidim/notifications_cell.rb +0 -1
- data/app/cells/decidim/user_conversation/conversation_header.erb +1 -1
- data/app/cells/decidim/user_conversation/show.erb +4 -2
- data/app/cells/decidim/user_conversations/conversation_item.erb +1 -1
- data/app/commands/decidim/create_editor_image.rb +41 -0
- data/app/controllers/concerns/decidim/disable_redirection_to_external_host.rb +15 -0
- data/app/controllers/concerns/decidim/safe_redirect.rb +14 -3
- data/app/controllers/decidim/application_controller.rb +1 -0
- data/app/controllers/decidim/cookie_policy_controller.rb +2 -0
- data/app/controllers/decidim/editor_images_controller.rb +47 -0
- data/app/controllers/decidim/user_activities_controller.rb +2 -1
- data/app/forms/decidim/editor_image_form.rb +16 -0
- data/app/helpers/decidim/amendments_helper.rb +1 -1
- data/app/helpers/decidim/application_helper.rb +2 -2
- data/app/helpers/decidim/messaging/conversation_helper.rb +32 -3
- data/app/helpers/decidim/resource_versions_helper.rb +1 -1
- data/app/helpers/decidim/sanitize_helper.rb +65 -0
- data/app/models/decidim/editor_image.rb +14 -0
- data/app/models/decidim/messaging/conversation.rb +9 -0
- data/app/models/decidim/participatory_space_private_user.rb +16 -0
- data/app/models/decidim/user.rb +3 -9
- data/app/models/decidim/user_base_entity.rb +24 -13
- data/app/models/decidim/user_group.rb +40 -0
- data/app/packs/entrypoints/decidim_core.js +1 -0
- data/app/packs/src/decidim/dialog_mode.js +143 -0
- data/app/packs/src/decidim/dialog_mode.test.js +168 -0
- data/app/packs/src/decidim/editor.js +56 -14
- data/app/packs/src/decidim/form_attachments.js +5 -0
- data/app/packs/src/decidim/geocoding/attach_input.js +11 -2
- data/app/packs/src/decidim/index.js +4 -0
- data/app/packs/src/decidim/input_emoji.js +10 -1
- data/app/packs/src/decidim/vendor/image-resize.min.js +3 -0
- data/app/packs/src/decidim/vendor/image-upload.min.js +8 -0
- data/app/packs/stylesheets/decidim/extras/_extras.scss +0 -1
- data/app/packs/stylesheets/decidim/extras/_quill.scss +7 -0
- data/app/packs/stylesheets/decidim/modules/_buttons.scss +11 -4
- data/app/packs/stylesheets/decidim/modules/_cards.scss +4 -0
- data/app/packs/stylesheets/decidim/modules/_layout.scss +1 -1
- data/app/packs/stylesheets/decidim/modules/_timeline.scss +1 -1
- data/app/presenters/decidim/nil_presenter.rb +2 -2
- data/app/presenters/decidim/notification_presenter.rb +25 -0
- data/app/presenters/decidim/official_author_presenter.rb +1 -1
- data/app/presenters/decidim/validation_errors_presenter.rb +27 -0
- data/app/queries/decidim/similar_emendations.rb +1 -1
- data/app/resolvers/decidim/core/metric_resolver.rb +1 -1
- data/app/services/decidim/activity_search.rb +2 -2
- data/app/services/decidim/email_notification_generator.rb +4 -1
- data/app/services/decidim/html_truncation.rb +130 -0
- data/app/services/decidim/open_data_exporter.rb +29 -5
- data/app/services/decidim/resource_search.rb +1 -1
- data/app/uploaders/decidim/editor_image_uploader.rb +6 -0
- data/app/validators/password_validator.rb +123 -0
- data/app/views/decidim/account/_password_fields.html.erb +1 -1
- data/app/views/decidim/devise/passwords/edit.html.erb +1 -1
- data/app/views/decidim/devise/registrations/new.html.erb +1 -1
- data/app/views/decidim/devise/shared/_omniauth_buttons_mini.html.erb +6 -4
- data/app/views/decidim/messaging/conversations/_conversation.html.erb +9 -3
- data/app/views/decidim/messaging/conversations/_messages.html.erb +8 -2
- data/app/views/decidim/messaging/conversations/_show.html.erb +10 -12
- data/app/views/decidim/messaging/conversations/show.html.erb +4 -2
- data/app/views/decidim/newsletters/show.html.erb +1 -1
- data/app/views/decidim/notification_mailer/event_received.html.erb +17 -0
- data/app/views/decidim/pages/_tabbed.html.erb +1 -1
- data/app/views/decidim/searches/_filters_small_view.html.erb +3 -3
- data/app/views/decidim/shared/_login_modal.html.erb +5 -5
- data/app/views/decidim/shared/_orders.html.erb +1 -1
- data/app/views/decidim/shared/_results_per_page.html.erb +1 -1
- data/app/views/decidim/shared/participatory_space_filters/_filters_small_view.html.erb +3 -3
- data/app/views/layouts/decidim/_application.html.erb +1 -12
- data/app/views/layouts/decidim/_head.html.erb +4 -0
- data/app/views/layouts/decidim/_language_chooser.html.erb +1 -1
- data/app/views/layouts/decidim/_meta_tags_config.html.erb +11 -0
- data/app/views/layouts/decidim/_wrapper.html.erb +1 -1
- data/config/brakeman.ignore +149 -0
- data/config/initializers/devise.rb +1 -1
- data/config/initializers/rack_attack.rb +23 -21
- data/config/locales/ar.yml +8 -0
- data/config/locales/ca.yml +43 -0
- data/config/locales/cs.yml +61 -0
- data/config/locales/en.yml +47 -0
- data/config/locales/es-MX.yml +42 -0
- data/config/locales/es-PY.yml +42 -0
- data/config/locales/es.yml +42 -0
- data/config/locales/eu.yml +27 -12
- data/config/locales/fi-plain.yml +42 -0
- data/config/locales/fi.yml +42 -0
- data/config/locales/fr-CA.yml +43 -0
- data/config/locales/fr.yml +75 -28
- data/config/locales/gl.yml +6 -0
- data/config/locales/it.yml +11 -0
- data/config/locales/ja.yml +85 -49
- data/config/locales/lb-LU.yml +1354 -0
- data/config/locales/lb.yml +1 -1
- data/config/locales/nl.yml +51 -0
- data/config/locales/pl.yml +5 -5
- data/config/locales/pt-BR.yml +1 -1
- data/config/locales/ro-RO.yml +275 -252
- data/config/locales/sv.yml +45 -2
- data/config/locales/val-ES.yml +1 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20210730112319_create_decidim_editor_images.rb +12 -0
- data/db/migrate/20211126183540_add_timestamps_to_content_blocks.rb +14 -0
- data/db/seeds.rb +16 -14
- data/lib/decidim/api/functions/user_entity_finder.rb +2 -1
- data/lib/decidim/api/functions/user_entity_list.rb +3 -1
- data/lib/decidim/api/input_sorts/component_input_sort.rb +1 -1
- data/lib/decidim/common_passwords.rb +56 -0
- data/lib/decidim/content_parsers/inline_images_parser.rb +68 -0
- data/lib/decidim/content_parsers.rb +1 -0
- data/lib/decidim/content_renderers/link_renderer.rb +85 -1
- data/lib/decidim/content_renderers/user_group_renderer.rb +1 -1
- data/lib/decidim/content_renderers/user_renderer.rb +1 -1
- data/lib/decidim/core/engine.rb +7 -12
- data/lib/decidim/core/test/factories.rb +7 -1
- data/lib/decidim/core/test/shared_examples/translated_event_examples.rb +131 -0
- data/lib/decidim/core/test.rb +1 -0
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/core.rb +33 -5
- data/lib/decidim/db/common-passwords.txt +128420 -0
- data/lib/decidim/etherpad/pad.rb +48 -0
- data/lib/decidim/etherpad.rb +7 -0
- data/lib/decidim/events/base_event.rb +18 -0
- data/lib/decidim/events/machine_translated_event.rb +36 -0
- data/lib/decidim/events/user_group_event.rb +1 -3
- data/lib/decidim/events.rb +1 -0
- data/lib/decidim/exporters/csv.rb +7 -7
- data/lib/decidim/faker/localized.rb +15 -6
- data/lib/decidim/form_builder.rb +14 -4
- data/lib/decidim/has_attachments.rb +11 -4
- data/lib/decidim/has_component.rb +4 -0
- data/lib/decidim/importers/import_manifest.rb +103 -3
- data/lib/decidim/organization_settings.rb +1 -1
- data/lib/decidim/paddable.rb +1 -9
- data/lib/decidim/participable.rb +5 -0
- data/lib/decidim/resourceable.rb +2 -9
- data/lib/decidim/searchable.rb +2 -2
- data/lib/decidim/settings_manifest.rb +2 -0
- data/lib/decidim/translatable_attributes.rb +6 -6
- data/lib/decidim/view_model.rb +10 -0
- data/lib/tasks/decidim_active_storage_migration_tasks.rake +68 -0
- data/lib/tasks/decidim_webpacker_tasks.rake +4 -10
- metadata +70 -78
- data/app/packs/stylesheets/decidim/extras/_social_icons_mini.scss +0 -11
|
@@ -3,14 +3,43 @@
|
|
|
3
3
|
module Decidim
|
|
4
4
|
module Messaging
|
|
5
5
|
module ConversationHelper
|
|
6
|
+
def conversation_name_for(users)
|
|
7
|
+
return content_tag(:span, t("decidim.profile.deleted"), class: "label label--small label--basic") if users.first.deleted?
|
|
8
|
+
|
|
9
|
+
content_tag = content_tag(:strong, users.first.name)
|
|
10
|
+
content_tag << tag.br
|
|
11
|
+
content_tag << content_tag(:span, "@#{users.first.nickname}", class: "muted")
|
|
12
|
+
content_tag
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def conversation_label_for(participants)
|
|
16
|
+
return t("title", scope: "decidim.messaging.conversations.show", usernames: username_list(participants)) unless participants.count == 1
|
|
17
|
+
|
|
18
|
+
chat_with_user = if participants.first.deleted?
|
|
19
|
+
t("decidim.profile.deleted")
|
|
20
|
+
else
|
|
21
|
+
"#{participants.first.name} (@#{participants.first.nickname})"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
"#{t("chat_with", scope: "decidim.messaging.conversations.show")} #{chat_with_user}"
|
|
25
|
+
end
|
|
26
|
+
|
|
6
27
|
#
|
|
7
28
|
# Generates a visualization of users for listing conversations threads
|
|
8
29
|
#
|
|
9
30
|
def username_list(users, shorten: false)
|
|
10
|
-
|
|
11
|
-
|
|
31
|
+
content_tags = []
|
|
32
|
+
first_users = shorten ? users.first(3) : users
|
|
33
|
+
deleted_user_tag = content_tag(:span, t("decidim.profile.deleted"), class: "label label--small label--basic")
|
|
34
|
+
first_users.each do |u|
|
|
35
|
+
content_tags.push(u.deleted? ? deleted_user_tag : content_tag(:strong, u.name))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return content_tags.join(", ") unless shorten
|
|
39
|
+
return content_tags.join(", ") unless users.count > 3
|
|
12
40
|
|
|
13
|
-
|
|
41
|
+
content_tags.push(content_tag(:strong, " + #{users.count - 3}"))
|
|
42
|
+
content_tags.join(", ")
|
|
14
43
|
end
|
|
15
44
|
|
|
16
45
|
#
|
|
@@ -13,7 +13,7 @@ module Decidim
|
|
|
13
13
|
#
|
|
14
14
|
# Returns a String.
|
|
15
15
|
def resource_version(resource, options = {})
|
|
16
|
-
return unless resource.respond_to?(:versions) && resource.
|
|
16
|
+
return unless resource.respond_to?(:versions) && resource.versions_count.positive?
|
|
17
17
|
|
|
18
18
|
html = []
|
|
19
19
|
html << resource_version_number(resource.versions_count)
|
|
@@ -5,6 +5,7 @@ module Decidim
|
|
|
5
5
|
module SanitizeHelper
|
|
6
6
|
def self.included(base)
|
|
7
7
|
base.include ActionView::Helpers::SanitizeHelper
|
|
8
|
+
base.include ActionView::Helpers::TagHelper
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
# Public: It sanitizes a user-inputted string with the
|
|
@@ -30,6 +31,10 @@ module Decidim
|
|
|
30
31
|
end
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
def decidim_sanitize_editor(html, options = {})
|
|
35
|
+
content_tag(:div, decidim_sanitize(html, options), class: %w(ql-editor ql-reset-decidim))
|
|
36
|
+
end
|
|
37
|
+
|
|
33
38
|
def decidim_html_escape(text)
|
|
34
39
|
ERB::Util.unwrapped_html_escape(text.to_str)
|
|
35
40
|
end
|
|
@@ -37,5 +42,65 @@ module Decidim
|
|
|
37
42
|
def decidim_url_escape(text)
|
|
38
43
|
decidim_html_escape(text).sub(/^javascript:/, "")
|
|
39
44
|
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Maintains the paragraphs and lists separations with their bullet points and
|
|
49
|
+
# list numberings where appropriate.
|
|
50
|
+
#
|
|
51
|
+
# Returns a String.
|
|
52
|
+
def sanitize_text(text)
|
|
53
|
+
add_line_feeds(sanitize_ordered_lists(sanitize_unordered_lists(text)))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def sanitize_unordered_lists(text)
|
|
57
|
+
text.gsub(%r{(?=.*</ul>)(?!.*?<li>.*?</ol>.*?</ul>)<li>}) { |li| "#{li}• " }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def sanitize_ordered_lists(text)
|
|
61
|
+
i = 0
|
|
62
|
+
|
|
63
|
+
text.gsub(%r{(?=.*</ol>)(?!.*?<li>.*?</ul>.*?</ol>)<li>}) do |li|
|
|
64
|
+
i += 1
|
|
65
|
+
|
|
66
|
+
li + "#{i}. "
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def add_line_feeds_to_paragraphs(text)
|
|
71
|
+
text.gsub("</p>") { |p| "#{p}\n\n" }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def add_line_feeds_to_list_items(text)
|
|
75
|
+
text.gsub("</li>") { |li| "#{li}\n" }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Adds line feeds after the paragraph and list item closing tags.
|
|
79
|
+
#
|
|
80
|
+
# Returns a String.
|
|
81
|
+
def add_line_feeds(text)
|
|
82
|
+
add_line_feeds_to_paragraphs(add_line_feeds_to_list_items(text))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def content_handle_locale(body, all_locales, extras, links, strip_tags)
|
|
86
|
+
handle_locales(body, all_locales) do |content|
|
|
87
|
+
content = strip_tags(sanitize_text(content)) if strip_tags
|
|
88
|
+
|
|
89
|
+
renderer = Decidim::ContentRenderers::HashtagRenderer.new(content)
|
|
90
|
+
content = renderer.render(links: links, extras: extras).html_safe
|
|
91
|
+
|
|
92
|
+
content = Decidim::ContentRenderers::LinkRenderer.new(content).render if links
|
|
93
|
+
content
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def render_sanitized_content(resource, method)
|
|
98
|
+
content = present(resource).send(method, links: true, strip_tags: !safe_content?)
|
|
99
|
+
content = simple_format(content, {}, sanitize: false)
|
|
100
|
+
|
|
101
|
+
return content unless safe_content?
|
|
102
|
+
|
|
103
|
+
decidim_sanitize_editor(content)
|
|
104
|
+
end
|
|
40
105
|
end
|
|
41
106
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
# Images attached to editors.
|
|
5
|
+
class EditorImage < ApplicationRecord
|
|
6
|
+
include Decidim::HasUploadValidations
|
|
7
|
+
|
|
8
|
+
belongs_to :author, foreign_key: :decidim_author_id, class_name: "Decidim::User"
|
|
9
|
+
belongs_to :organization, foreign_key: :decidim_organization_id, class_name: "Decidim::Organization"
|
|
10
|
+
|
|
11
|
+
has_one_attached :file
|
|
12
|
+
validates_upload :file, uploader: Decidim::EditorImageUploader
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -131,6 +131,15 @@ module Decidim
|
|
|
131
131
|
blocked.blank?
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
#
|
|
135
|
+
# Given a user, returns if ALL the interlocutors have their accounts deleted
|
|
136
|
+
#
|
|
137
|
+
# @return Boolean
|
|
138
|
+
#
|
|
139
|
+
def with_deleted_users?(user)
|
|
140
|
+
interlocutors(user).all?(&:deleted?)
|
|
141
|
+
end
|
|
142
|
+
|
|
134
143
|
#
|
|
135
144
|
# Given a user, returns if the user is participating in the conversation
|
|
136
145
|
# for groups being part of a conversation all their admin member are accepted
|
|
@@ -22,6 +22,22 @@ module Decidim
|
|
|
22
22
|
Decidim::AdminLog::ParticipatorySpacePrivateUserPresenter
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
ransacker :name do
|
|
26
|
+
Arel.sql(%{("decidim_users"."name")::text})
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
ransacker :email do
|
|
30
|
+
Arel.sql(%{("decidim_users"."email")::text})
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
ransacker :invitation_sent_at do
|
|
34
|
+
Arel.sql(%{("invitation_sent_at")::text})
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
ransacker :invitation_accepted_at do
|
|
38
|
+
Arel.sql(%{("invitation_accepted_at")::text})
|
|
39
|
+
end
|
|
40
|
+
|
|
25
41
|
private
|
|
26
42
|
|
|
27
43
|
# Private: check if the participatory space and the user have the same organization
|
data/app/models/decidim/user.rb
CHANGED
|
@@ -63,17 +63,11 @@ module Decidim
|
|
|
63
63
|
scope :officialized, -> { where.not(officialized_at: nil) }
|
|
64
64
|
scope :not_officialized, -> { where(officialized_at: nil) }
|
|
65
65
|
|
|
66
|
-
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
|
67
|
-
scope :not_confirmed, -> { where(confirmed_at: nil) }
|
|
68
|
-
|
|
69
|
-
scope :blocked, -> { where(blocked: true) }
|
|
70
|
-
scope :not_blocked, -> { where(blocked: false) }
|
|
71
|
-
|
|
72
66
|
scope :interested_in_scopes, lambda { |scope_ids|
|
|
73
67
|
actual_ids = scope_ids.select(&:presence)
|
|
74
68
|
if actual_ids.count.positive?
|
|
75
69
|
ids = actual_ids.map(&:to_i).join(",")
|
|
76
|
-
where("extended_data->'interested_scopes' @> ANY('{#{ids}}')")
|
|
70
|
+
where(Arel.sql("extended_data->'interested_scopes' @> ANY('{#{ids}}')").to_s)
|
|
77
71
|
else
|
|
78
72
|
# Do not apply the scope filter when there are scope ids available. Note
|
|
79
73
|
# that the active record scope must always return an active record
|
|
@@ -92,8 +86,8 @@ module Decidim
|
|
|
92
86
|
A: :name,
|
|
93
87
|
datetime: :created_at
|
|
94
88
|
},
|
|
95
|
-
index_on_create: ->(user) { !user.deleted? },
|
|
96
|
-
index_on_update: ->(user) { !user.deleted? })
|
|
89
|
+
index_on_create: ->(user) { !(user.deleted? || user.blocked?) },
|
|
90
|
+
index_on_update: ->(user) { !(user.deleted? || user.blocked?) })
|
|
97
91
|
|
|
98
92
|
before_save :ensure_encrypted_password
|
|
99
93
|
|
|
@@ -25,26 +25,37 @@ module Decidim
|
|
|
25
25
|
|
|
26
26
|
validates :name, format: { with: REGEXP_NAME }
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
|
29
|
+
scope :not_confirmed, -> { where(confirmed_at: nil) }
|
|
30
|
+
|
|
31
|
+
scope :blocked, -> { where(blocked: true) }
|
|
32
|
+
scope :not_blocked, -> { where(blocked: false) }
|
|
33
|
+
|
|
34
|
+
# Public: Returns a collection with all the public entities this user is following.
|
|
29
35
|
#
|
|
30
36
|
# This can't be done as with a `has_many :following, through: :following_follows`
|
|
31
37
|
# since it's a polymorphic relation and Rails doesn't know how to load it. With
|
|
32
38
|
# this implementation we only query the database once for each kind of following.
|
|
33
39
|
#
|
|
34
40
|
# Returns an Array of Decidim::Followable
|
|
35
|
-
def
|
|
36
|
-
@
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
grouped_followings.flat_map do |type, ids|
|
|
45
|
-
type.constantize.where(id: ids)
|
|
46
|
-
end
|
|
41
|
+
def public_followings
|
|
42
|
+
@public_followings ||= following_follows.select("array_agg(decidim_followable_id)")
|
|
43
|
+
.group(:decidim_followable_type)
|
|
44
|
+
.pluck(:decidim_followable_type, "array_agg(decidim_followable_id)")
|
|
45
|
+
.to_h
|
|
46
|
+
.flat_map do |type, ids|
|
|
47
|
+
only_public(type.constantize, ids)
|
|
47
48
|
end
|
|
48
49
|
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def only_public(klass, ids)
|
|
54
|
+
scope = klass.where(id: ids)
|
|
55
|
+
scope = scope.public_spaces if klass.try(:participatory_space?)
|
|
56
|
+
scope = scope.includes(:component) if klass.try(:has_component?)
|
|
57
|
+
scope = scope.filter(&:visible?) if klass.method_defined?(:visible?)
|
|
58
|
+
scope
|
|
59
|
+
end
|
|
49
60
|
end
|
|
50
61
|
end
|
|
@@ -144,6 +144,46 @@ module Decidim
|
|
|
144
144
|
@unread_messages_count_for[user.id] ||= Decidim::Messaging::Conversation.user_collection(self).unread_messages_by(user).count
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
+
def self.state_eq(value)
|
|
148
|
+
send(value.to_sym) if %w(all pending rejected verified).include?(value)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def self.ransackable_scopes(_auth = nil)
|
|
152
|
+
[:state_eq]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
scope :sort_by_users_count_asc, lambda {
|
|
156
|
+
order("users_count ASC NULLS FIRST")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
scope :sort_by_users_count_desc, lambda {
|
|
160
|
+
order("users_count DESC NULLS LAST")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def self.sort_by_document_number_asc
|
|
164
|
+
order(Arel.sql("extended_data->>'document_number' ASC"))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def self.sort_by_document_number_desc
|
|
168
|
+
order(Arel.sql("extended_data->>'document_number' DESC"))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def self.sort_by_phone_asc
|
|
172
|
+
order(Arel.sql("extended_data->>'phone' ASC"))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def self.sort_by_phone_desc
|
|
176
|
+
order(Arel.sql("extended_data->>'phone' DESC"))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def self.sort_by_state_asc
|
|
180
|
+
order(Arel.sql("extended_data->>'rejected_at' ASC, extended_data->>'verified_at' ASC, deleted_at ASC"))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def self.sort_by_state_desc
|
|
184
|
+
order(Arel.sql("extended_data->>'rejected_at' DESC, extended_data->>'verified_at' DESC, deleted_at DESC"))
|
|
185
|
+
end
|
|
186
|
+
|
|
147
187
|
private
|
|
148
188
|
|
|
149
189
|
# Private: Checks if the state user group is correct.
|
|
@@ -31,6 +31,7 @@ import "src/decidim/account_form"
|
|
|
31
31
|
import "src/decidim/data_picker"
|
|
32
32
|
import "src/decidim/dropdowns_menus"
|
|
33
33
|
import "src/decidim/append_redirect_url_to_modals"
|
|
34
|
+
import "src/decidim/form_attachments"
|
|
34
35
|
import "src/decidim/form_validator"
|
|
35
36
|
import "src/decidim/ajax_modals"
|
|
36
37
|
import "src/decidim/conferences"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const focusGuardClass = "focusguard";
|
|
2
|
+
const focusableNodes = ["A", "IFRAME", "OBJECT", "EMBED"];
|
|
3
|
+
const focusableDisableableNodes = ["BUTTON", "INPUT", "TEXTAREA", "SELECT"];
|
|
4
|
+
|
|
5
|
+
const isFocusGuard = (element) => {
|
|
6
|
+
return element.classList.contains(focusGuardClass);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const isFocusable = (element) => {
|
|
10
|
+
if (focusableNodes.indexOf(element.nodeName) > -1) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (focusableDisableableNodes.indexOf(element.nodeName) > -1 || element.getAttribute("contenteditable")) {
|
|
14
|
+
if (element.getAttribute("disabled")) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tabindex = parseInt(element.getAttribute("tabindex"), 10);
|
|
21
|
+
if (!isNaN(tabindex) && tabindex >= 0) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const createFocusGuard = (position) => {
|
|
29
|
+
return $(`<div class="${focusGuardClass}" data-position="${position}" tabindex="0" aria-hidden="true"></div>`);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleContainerFocus = ($container, $guard) => {
|
|
33
|
+
const $reveal = $(".reveal:visible:last", $container);
|
|
34
|
+
if ($reveal.length > 0) {
|
|
35
|
+
handleContainerFocus($reveal, $guard);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const $nodes = $("*:visible", $container);
|
|
40
|
+
let $target = null;
|
|
41
|
+
|
|
42
|
+
if ($guard.data("position") === "start") {
|
|
43
|
+
// Focus at the start guard, so focus the first focusable element after that
|
|
44
|
+
for (let ind = 0; ind < $nodes.length; ind += 1) {
|
|
45
|
+
if (!isFocusGuard($nodes[ind]) && isFocusable($nodes[ind])) {
|
|
46
|
+
$target = $($nodes[ind]);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
// Focus at the end guard, so focus the first focusable element after that
|
|
52
|
+
for (let ind = $nodes.length - 1; ind >= 0; ind -= 1) {
|
|
53
|
+
if (!isFocusGuard($nodes[ind]) && isFocusable($nodes[ind])) {
|
|
54
|
+
$target = $($nodes[ind]);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if ($target) {
|
|
61
|
+
$target.trigger("focus");
|
|
62
|
+
} else {
|
|
63
|
+
// If no focusable element was found, blur the guard focus
|
|
64
|
+
$guard.blur();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A method to enable the dialog mode for the given dialog(s).
|
|
70
|
+
*
|
|
71
|
+
* This should be called when the dialog is opened. It implements two things for
|
|
72
|
+
* the dialog:
|
|
73
|
+
* 1. It places the focus to the title element making sure the screen reader
|
|
74
|
+
* focuses in the correct position of the document. Otherwise some screen
|
|
75
|
+
* readers continue reading outside of the document.
|
|
76
|
+
* 2. Document "tab guards" that force the keyboard focus within the modal when
|
|
77
|
+
* the user is using keyboard or keyboard emulating devices for browsing the
|
|
78
|
+
* document.
|
|
79
|
+
*
|
|
80
|
+
* The "tab guards" are added at the top and bottom of the document to keep the
|
|
81
|
+
* user's focus within the dialog if they accidentally or intentionally place
|
|
82
|
+
* the focus outside of the document, e.g. in different window or in the browser
|
|
83
|
+
* address bar. They guard the focus on both sides of the document returning
|
|
84
|
+
* focus back to the first or last focusable element within the dialog.
|
|
85
|
+
*
|
|
86
|
+
* @param {jQuery} $dialogs The jQuery element(s) to apply the mode for.
|
|
87
|
+
* @return {Void} Nothing
|
|
88
|
+
*/
|
|
89
|
+
export default ($dialogs) => {
|
|
90
|
+
$dialogs.each((_i, dialog) => {
|
|
91
|
+
const $dialog = $(dialog);
|
|
92
|
+
|
|
93
|
+
const $container = $("body");
|
|
94
|
+
const $title = $(".reveal__title:first", $dialog);
|
|
95
|
+
|
|
96
|
+
if ($title.length > 0) {
|
|
97
|
+
// Focus on the title to make the screen reader to start reading the
|
|
98
|
+
// content within the modal.
|
|
99
|
+
$title.attr("tabindex", $title.attr("tabindex") || -1);
|
|
100
|
+
$title.trigger("focus");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Once the final modal closes, remove the focus guards from the container
|
|
104
|
+
$dialog.off("closed.zf.reveal.focusguard").on("closed.zf.reveal.focusguard", () => {
|
|
105
|
+
$dialog.off("closed.zf.reveal.focusguard");
|
|
106
|
+
|
|
107
|
+
// After the last dialog is closed, the tab guards should be removed.
|
|
108
|
+
// Note that there may be multiple dialogs open on top of each other at
|
|
109
|
+
// the same time.
|
|
110
|
+
if ($(".reveal:visible", $container).length < 1) {
|
|
111
|
+
$(`> .${focusGuardClass}`, $container).remove();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Check if the guards already exists due to some other dialog
|
|
116
|
+
const $guards = $(`> .${focusGuardClass}`, $container);
|
|
117
|
+
if ($guards.length > 0) {
|
|
118
|
+
// Make sure the guards are the first and last element as there have
|
|
119
|
+
// been changes in the DOM.
|
|
120
|
+
$guards.each((_j, guard) => {
|
|
121
|
+
const $guard = $(guard);
|
|
122
|
+
if ($guard.data("position") === "start") {
|
|
123
|
+
$container.prepend($guard);
|
|
124
|
+
} else {
|
|
125
|
+
$container.append($guard);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Add guards at the start and end of the document and attach their focus
|
|
133
|
+
// listeners
|
|
134
|
+
const $startGuard = createFocusGuard("start");
|
|
135
|
+
const $endGuard = createFocusGuard("end");
|
|
136
|
+
|
|
137
|
+
$container.prepend($startGuard);
|
|
138
|
+
$container.append($endGuard);
|
|
139
|
+
|
|
140
|
+
$startGuard.on("focus", () => handleContainerFocus($container, $startGuard));
|
|
141
|
+
$endGuard.on("focus", () => handleContainerFocus($container, $endGuard));
|
|
142
|
+
});
|
|
143
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/* global jest, global */
|
|
2
|
+
|
|
3
|
+
// Mock jQuery because the visibility indicator works differently within jest.
|
|
4
|
+
// This fixes jQuery reporting $(".element").is(":visible") correctly during the
|
|
5
|
+
// tests and within foundation, too.
|
|
6
|
+
jest.mock("jquery", () => {
|
|
7
|
+
const jq = jest.requireActual("jquery");
|
|
8
|
+
|
|
9
|
+
jq.expr.pseudos.visible = (elem) => {
|
|
10
|
+
const display = global.window.getComputedStyle(elem).display;
|
|
11
|
+
return ["inline", "block", "inline-block"].includes(display);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return jq;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
import $ from "jquery"; // eslint-disable-line id-length
|
|
18
|
+
import "foundation-sites";
|
|
19
|
+
|
|
20
|
+
import dialogMode from "./dialog_mode.js";
|
|
21
|
+
|
|
22
|
+
describe("dialogMode", () => {
|
|
23
|
+
const content = `
|
|
24
|
+
<div class="reveal" id="test-modal" data-reveal aria-modal="true" aria-labelledby="test-modal-label">
|
|
25
|
+
<div class="reveal__header">
|
|
26
|
+
<h3 id="test-modal-label" class="reveal__title">Testing modal</h3>
|
|
27
|
+
<button class="close-button" data-close aria-label="Close window"
|
|
28
|
+
type="button">
|
|
29
|
+
<span aria-hidden="true">×</span>
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="row">
|
|
33
|
+
<div class="columns medium-4 medium-centered">
|
|
34
|
+
<p>Here is some content within the modal.</p>
|
|
35
|
+
<button type="button" id="test-modal-button">Button at the bottom</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="reveal" id="test-modal-2" data-reveal aria-modal="true" aria-labelledby="test-modal-2-label">
|
|
41
|
+
<div class="reveal__header">
|
|
42
|
+
<h3 id="test-modal-2-label" class="reveal__title">Other testing modal</h3>
|
|
43
|
+
<button class="close-button" data-close aria-label="Close window"
|
|
44
|
+
type="button">
|
|
45
|
+
<span aria-hidden="true">×</span>
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="row">
|
|
49
|
+
<div class="columns medium-4 medium-centered">
|
|
50
|
+
<p>Here is some content within the other modal.</p>
|
|
51
|
+
<button type="button" id="test-modal-2-button">Button at the bottom</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
$("body").html(content);
|
|
59
|
+
|
|
60
|
+
$(document).foundation();
|
|
61
|
+
|
|
62
|
+
// Make sure all reveals are hidden by default so that their visibility is
|
|
63
|
+
// correctly reported always.
|
|
64
|
+
$(".reveal").css("display", "none");
|
|
65
|
+
|
|
66
|
+
$(document).on("open.zf.reveal", (ev) => {
|
|
67
|
+
dialogMode($(ev.target));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("focuses the title", () => {
|
|
72
|
+
$("#test-modal").foundation("open");
|
|
73
|
+
|
|
74
|
+
const $focused = $(document.activeElement);
|
|
75
|
+
expect($focused.is($("#test-modal-label"))).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("adds the tab guads on both sides of the document", () => {
|
|
79
|
+
$("#test-modal").foundation("open");
|
|
80
|
+
|
|
81
|
+
const $first = $("body *:first");
|
|
82
|
+
const $last = $("body *:last");
|
|
83
|
+
|
|
84
|
+
expect($first[0].outerHTML).toEqual(
|
|
85
|
+
'<div class="focusguard" data-position="start" tabindex="0" aria-hidden="true"></div>'
|
|
86
|
+
);
|
|
87
|
+
expect($last[0].outerHTML).toEqual(
|
|
88
|
+
'<div class="focusguard" data-position="end" tabindex="0" aria-hidden="true"></div>'
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("removes the tab guards when the modal is closed", () => {
|
|
93
|
+
const $modal = $("#test-modal");
|
|
94
|
+
$modal.foundation("open");
|
|
95
|
+
$modal.foundation("close");
|
|
96
|
+
|
|
97
|
+
expect($(".focusguard").length).toEqual(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("focuses the first focusable element when the start tab guard gets focus", () => {
|
|
101
|
+
const $modal = $("#test-modal");
|
|
102
|
+
$modal.foundation("open");
|
|
103
|
+
|
|
104
|
+
$(".focusguard[data-position='start']").trigger("focus");
|
|
105
|
+
|
|
106
|
+
const $focused = $(document.activeElement);
|
|
107
|
+
expect($focused.is($("#test-modal .close-button"))).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("focuses the last focusable element when the end tab guard gets focus", () => {
|
|
111
|
+
const $modal = $("#test-modal");
|
|
112
|
+
$modal.foundation("open");
|
|
113
|
+
|
|
114
|
+
$(".focusguard[data-position='end']").trigger("focus");
|
|
115
|
+
|
|
116
|
+
const $focused = $(document.activeElement);
|
|
117
|
+
expect($focused.is($("#test-modal-button"))).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("when multiple modals are opened", () => {
|
|
121
|
+
it("adds the tab guads only once", () => {
|
|
122
|
+
$("#test-modal").foundation("open");
|
|
123
|
+
$("#test-modal-2").foundation("open");
|
|
124
|
+
|
|
125
|
+
expect($(".focusguard[data-position='start']").length).toEqual(1);
|
|
126
|
+
expect($(".focusguard[data-position='end']").length).toEqual(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("does not remove the tab guards when modal is closed but there is still another modal open", () => {
|
|
130
|
+
$("#test-modal").foundation("open");
|
|
131
|
+
$("#test-modal-2").foundation("open");
|
|
132
|
+
$("#test-modal-2").foundation("close");
|
|
133
|
+
|
|
134
|
+
expect($(".focusguard[data-position='start']").length).toEqual(1);
|
|
135
|
+
expect($(".focusguard[data-position='end']").length).toEqual(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("removes the tab guards when the last modal is closed", () => {
|
|
139
|
+
$("#test-modal").foundation("open");
|
|
140
|
+
$("#test-modal-2").foundation("open");
|
|
141
|
+
$("#test-modal-2").foundation("close");
|
|
142
|
+
$("#test-modal").foundation("close");
|
|
143
|
+
|
|
144
|
+
expect($(".focusguard").length).toEqual(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("within the active modal", () => {
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
$("#test-modal").foundation("open");
|
|
150
|
+
$("#test-modal-2").foundation("open");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("focuses the first focusable element when the start tab guard gets focus", () => {
|
|
154
|
+
$(".focusguard[data-position='start']").trigger("focus");
|
|
155
|
+
|
|
156
|
+
const $focused = $(document.activeElement);
|
|
157
|
+
expect($focused.is($("#test-modal-2 .close-button"))).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("focuses the last focusable element when the end tab guard gets focus", () => {
|
|
161
|
+
$(".focusguard[data-position='end']").trigger("focus");
|
|
162
|
+
|
|
163
|
+
const $focused = $(document.activeElement);
|
|
164
|
+
expect($focused.is($("#test-modal-2-button"))).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|