From 85bce8f3ea918ac5678a44a72da7d412996f978a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Mon, 4 Mar 2024 16:07:06 +0100 Subject: [PATCH 01/19] Fix showing announcement when comments are disabled --- .../cells/decidim/comments/comments_cell.rb | 2 +- .../test/shared_examples/comments_examples.rb | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/decidim-comments/app/cells/decidim/comments/comments_cell.rb b/decidim-comments/app/cells/decidim/comments/comments_cell.rb index 79cc298661940..af497a65464d2 100644 --- a/decidim-comments/app/cells/decidim/comments/comments_cell.rb +++ b/decidim-comments/app/cells/decidim/comments/comments_cell.rb @@ -27,7 +27,7 @@ def comments_loading def blocked_comments_warning return unless comments_blocked? - return unless user_comments_blocked? + return if user_comments_blocked? render :blocked_comments_warning end diff --git a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb index c44abbb88f33b..7e37a0cf15104 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb @@ -119,6 +119,20 @@ visit resource_path expect(page).to have_no_css(".add-comment form") end + + context "when comments are blocked" do + let(:active_step_id) { component.participatory_space.active_step.id } + + before do + component.update!(step_settings: { active_step_id => { comments_blocked: true } }) + end + + it "shows a message indicating that comments are disabled" do + visit resource_path + expect(page).to have_content("Comments are disabled at this time") + expect(page).to have_no_content("You need to be verified to comment at this moment") + end + end end context "when authenticated" do @@ -131,6 +145,20 @@ expect(page).to have_css(".add-comment form") end + context "when comments are blocked" do + let(:active_step_id) { component.participatory_space.active_step.id } + + before do + component.update!(step_settings: { active_step_id => { comments_blocked: true } }) + end + + it "shows a message indicating that comments are disabled" do + visit resource_path + expect(page).to have_content("Comments are disabled at this time") + expect(page).to have_no_content("You need to be verified to comment at this moment") + end + end + describe "when using emojis" do before do within_language_menu do From fdcc1ebe64420d1da34c38e2c0c287d5ffe7b6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Mon, 4 Mar 2024 16:07:32 +0100 Subject: [PATCH 02/19] Wait for the comment thread loading before checking for the form --- .../lib/decidim/core/test/shared_examples/comments_examples.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb index 7e37a0cf15104..feada70e86b3a 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb @@ -117,7 +117,8 @@ context "when not authenticated" do it "does not show form to add comments to user" do visit resource_path - expect(page).to have_no_css(".add-comment form") + expect(page).not_to have_selector(".add-comment form") + expect(page).to have_selector(".comment-thread") end context "when comments are blocked" do From 8f6e123ff0f01f02a5cb428cf0174769214ec6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Mon, 4 Mar 2024 16:07:50 +0100 Subject: [PATCH 03/19] Add a check for the verification announcement --- .../test/shared_examples/comments_examples.rb | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb index feada70e86b3a..a9d2c49f9de47 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb @@ -160,6 +160,32 @@ end end + context "when user is not authorized to comment" do + let(:permissions) do + { + comment: { + authorization_handlers: { + "dummy_authorization_handler" => { "options" => {} } + } + } + } + end + + before do + organization.available_authorizations = ["dummy_authorization_handler"] + organization.save! + commentable.create_resource_permission(permissions:) + allow(commentable).to receive(:user_allowed_to_comment?).with(user).and_return(false) + allow(commentable).to receive(:user_authorized_to_comment?).with(user).and_return(true) + end + + it "shows a message indicating that comments are restricted" do + visit resource_path + expect(page).to have_no_content("Comments are disabled at this time") + expect(page).to have_content("You need to be verified to comment at this moment") + end + end + describe "when using emojis" do before do within_language_menu do From 112e9d997cd060b1766a73146a4b70942037e101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Tue, 5 Mar 2024 13:28:44 +0100 Subject: [PATCH 04/19] Extract comments blocked to a shared example This is done mainly because some modules (such as Initiatives) don't have the feature for blocking the comments in the steps (as they don't have steps as a Participatory Process) --- .../spec/system/comments_spec.rb | 7 ++ decidim-budgets/spec/system/comments_spec.rb | 7 ++ decidim-comments/spec/system/comments_spec.rb | 10 ++- .../test/shared_examples/comments_examples.rb | 71 +++++++++++-------- decidim-debates/spec/system/comments_spec.rb | 7 ++ decidim-meetings/spec/system/comments_spec.rb | 7 ++ .../spec/system/comments_spec.rb | 7 ++ 7 files changed, 86 insertions(+), 30 deletions(-) diff --git a/decidim-accountability/spec/system/comments_spec.rb b/decidim-accountability/spec/system/comments_spec.rb index ffab4d87c4f8a..e84d43a86accf 100644 --- a/decidim-accountability/spec/system/comments_spec.rb +++ b/decidim-accountability/spec/system/comments_spec.rb @@ -9,4 +9,11 @@ let(:resource_path) { resource_locator(commentable).path } include_examples "comments" + + context "with comments blocked" do + let!(:component) { create(:component, manifest_name: :accountability, participatory_space:, organization:) } + let(:participatory_space) { create(:participatory_process, :with_steps, organization:) } + + include_examples "comments blocked" + end end diff --git a/decidim-budgets/spec/system/comments_spec.rb b/decidim-budgets/spec/system/comments_spec.rb index 00f5b3a48b38b..c0c9e35723e3e 100644 --- a/decidim-budgets/spec/system/comments_spec.rb +++ b/decidim-budgets/spec/system/comments_spec.rb @@ -10,6 +10,13 @@ include_examples "comments" + context "with comments blocked" do + let!(:component) { create(:budgets_component, participatory_space:, organization:) } + let(:participatory_space) { create(:participatory_process, :with_steps, organization:) } + + include_examples "comments blocked" + end + context "when requesting the comments index with a non-XHR request" do it "redirects the user to the correct commentable path" do visit decidim_comments.comments_path(commentable_gid: commentable.to_signed_global_id.to_s) diff --git a/decidim-comments/spec/system/comments_spec.rb b/decidim-comments/spec/system/comments_spec.rb index 6f7cd8dfa9397..d736ca501add3 100644 --- a/decidim-comments/spec/system/comments_spec.rb +++ b/decidim-comments/spec/system/comments_spec.rb @@ -4,10 +4,16 @@ describe "Comments" do let!(:component) { create(:component, manifest_name: :dummy, organization:) } - let!(:author) { create(:user, :confirmed, organization:) } - let!(:commentable) { create(:dummy_resource, component:, author:) } + let!(:commentable) { create(:dummy_resource, component:) } let(:resource_path) { resource_locator(commentable).path } include_examples "comments" + + context "with comments blocked" do + let!(:component) { create(:component, manifest_name: :dummy, participatory_space:, organization:) } + let(:participatory_space) { create(:participatory_process, :with_steps, organization:) } + + include_examples "comments blocked" + end end diff --git a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb index a9d2c49f9de47..2af1c1ab6d45d 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb @@ -120,20 +120,6 @@ expect(page).not_to have_selector(".add-comment form") expect(page).to have_selector(".comment-thread") end - - context "when comments are blocked" do - let(:active_step_id) { component.participatory_space.active_step.id } - - before do - component.update!(step_settings: { active_step_id => { comments_blocked: true } }) - end - - it "shows a message indicating that comments are disabled" do - visit resource_path - expect(page).to have_content("Comments are disabled at this time") - expect(page).to have_no_content("You need to be verified to comment at this moment") - end - end end context "when authenticated" do @@ -146,20 +132,6 @@ expect(page).to have_css(".add-comment form") end - context "when comments are blocked" do - let(:active_step_id) { component.participatory_space.active_step.id } - - before do - component.update!(step_settings: { active_step_id => { comments_blocked: true } }) - end - - it "shows a message indicating that comments are disabled" do - visit resource_path - expect(page).to have_content("Comments are disabled at this time") - expect(page).to have_no_content("You need to be verified to comment at this moment") - end - end - context "when user is not authorized to comment" do let(:permissions) do { @@ -1030,3 +1002,46 @@ end end end + +shared_examples "comments blocked" do + context "when not authenticated" do + context "when comments are blocked" do + let(:active_step_id) { component.participatory_space.active_step.id } + + before do + component.update!(step_settings: { active_step_id => { comments_blocked: true } }) + end + + it "shows a message indicating that comments are disabled" do + visit resource_path + expect(page).to have_content("Comments are disabled at this time") + expect(page).to have_no_content("You need to be verified to comment at this moment") + end + end + end + + context "when authenticated" do + let!(:organization) { create(:organization) } + let!(:user) { create(:user, :confirmed, organization:) } + let!(:comments) { create_list(:comment, 3, commentable:) } + + before do + login_as user, scope: :user + visit resource_path + end + + context "when comments are blocked" do + let(:active_step_id) { component.participatory_space.active_step.id } + + before do + component.update!(step_settings: { active_step_id => { comments_blocked: true } }) + end + + it "shows a message indicating that comments are disabled" do + visit resource_path + expect(page).to have_content("Comments are disabled at this time") + expect(page).to have_no_content("You need to be verified to comment at this moment") + end + end + end +end diff --git a/decidim-debates/spec/system/comments_spec.rb b/decidim-debates/spec/system/comments_spec.rb index 0581d1c7908d9..6fa4db1fd1d8c 100644 --- a/decidim-debates/spec/system/comments_spec.rb +++ b/decidim-debates/spec/system/comments_spec.rb @@ -9,4 +9,11 @@ let(:resource_path) { resource_locator(commentable).path } include_examples "comments" + + context "with comments blocked" do + let!(:component) { create(:debates_component, participatory_space:, organization:) } + let(:participatory_space) { create(:participatory_process, :with_steps, organization:) } + + include_examples "comments blocked" + end end diff --git a/decidim-meetings/spec/system/comments_spec.rb b/decidim-meetings/spec/system/comments_spec.rb index faa0350899bba..5198ef191b8c6 100644 --- a/decidim-meetings/spec/system/comments_spec.rb +++ b/decidim-meetings/spec/system/comments_spec.rb @@ -20,4 +20,11 @@ let(:resource_path) { resource_locator(commentable).path } include_examples "comments" + + context "with comments blocked" do + let!(:component) { create(:component, manifest_name: :meetings, participatory_space:, organization:) } + let(:participatory_space) { create(:participatory_process, :with_steps, organization:) } + + include_examples "comments blocked" + end end diff --git a/decidim-proposals/spec/system/comments_spec.rb b/decidim-proposals/spec/system/comments_spec.rb index 3472a9f96d094..f3bf13b67a74d 100644 --- a/decidim-proposals/spec/system/comments_spec.rb +++ b/decidim-proposals/spec/system/comments_spec.rb @@ -21,4 +21,11 @@ end include_examples "comments" + + context "with comments blocked" do + let!(:component) { create(:proposal_component, participatory_space:, organization:) } + let(:participatory_space) { create(:participatory_process, :with_steps, organization:) } + + include_examples "comments blocked" + end end From dd184f8a9fb129486cb1a5c6a1c422af2d456b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Tue, 5 Mar 2024 13:34:16 +0100 Subject: [PATCH 05/19] Add missing spec for blogs module --- decidim-blogs/spec/system/comments_spec.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 decidim-blogs/spec/system/comments_spec.rb diff --git a/decidim-blogs/spec/system/comments_spec.rb b/decidim-blogs/spec/system/comments_spec.rb new file mode 100644 index 0000000000000..d551198c426f6 --- /dev/null +++ b/decidim-blogs/spec/system/comments_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Comments", perform_enqueued: true do + let!(:component) { create(:post_component, organization:) } + let!(:commentable) { create(:post, component:) } + + let(:resource_path) { resource_locator(commentable).path } + + include_examples "comments" + + context "with comments blocked" do + let!(:component) { create(:post_component, participatory_space:, organization:) } + let(:participatory_space) { create(:participatory_process, :with_steps, organization:) } + + include_examples "comments blocked" + end +end From 2930d3dcc774c0c61569c309f58fd929fac39cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Wed, 6 Mar 2024 10:21:12 +0100 Subject: [PATCH 06/19] Fix specs --- decidim-comments/spec/system/comments_spec.rb | 7 ------- decidim-dev/app/models/decidim/dev/dummy_resource.rb | 4 +++- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/decidim-comments/spec/system/comments_spec.rb b/decidim-comments/spec/system/comments_spec.rb index d736ca501add3..36e8789643674 100644 --- a/decidim-comments/spec/system/comments_spec.rb +++ b/decidim-comments/spec/system/comments_spec.rb @@ -9,11 +9,4 @@ let(:resource_path) { resource_locator(commentable).path } include_examples "comments" - - context "with comments blocked" do - let!(:component) { create(:component, manifest_name: :dummy, participatory_space:, organization:) } - let(:participatory_space) { create(:participatory_process, :with_steps, organization:) } - - include_examples "comments blocked" - end end diff --git a/decidim-dev/app/models/decidim/dev/dummy_resource.rb b/decidim-dev/app/models/decidim/dev/dummy_resource.rb index 4568cca4cd358..267e5d9043cb7 100644 --- a/decidim-dev/app/models/decidim/dev/dummy_resource.rb +++ b/decidim-dev/app/models/decidim/dev/dummy_resource.rb @@ -63,7 +63,9 @@ def commentable? # Public: Whether the object can have new comments or not. def user_allowed_to_comment?(user) - component.can_participate_in_space?(user) + return unless component.can_participate_in_space?(user) + + ActionAuthorizer.new(user, "comment", component, self).authorize.ok? end # Public: Whether the object can have new comment votes or not. From 2b563877ef766909b6e868068a5a44d04fa25e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Thu, 23 May 2024 09:14:09 +0200 Subject: [PATCH 07/19] Fix rubocop offenses --- .../decidim/core/test/shared_examples/comments_examples.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb index 2af1c1ab6d45d..bc2d23246ee84 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/comments_examples.rb @@ -117,8 +117,8 @@ context "when not authenticated" do it "does not show form to add comments to user" do visit resource_path - expect(page).not_to have_selector(".add-comment form") - expect(page).to have_selector(".comment-thread") + expect(page).to have_no_css(".add-comment form") + expect(page).to have_css(".comment-thread") end end From bbf0cf4fabe37ed6fa9ee5fca10c66da3663bf57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Thu, 23 May 2024 10:52:59 +0200 Subject: [PATCH 08/19] Fix spec --- .../spec/cells/decidim/comments/comments_cell_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-comments/spec/cells/decidim/comments/comments_cell_spec.rb b/decidim-comments/spec/cells/decidim/comments/comments_cell_spec.rb index b56665b99cc42..4dbcf84c5a802 100644 --- a/decidim-comments/spec/cells/decidim/comments/comments_cell_spec.rb +++ b/decidim-comments/spec/cells/decidim/comments/comments_cell_spec.rb @@ -101,7 +101,7 @@ module Decidim::Comments before do comment # Create the comment before disabling comments allow(commentable).to receive(:accepts_new_comments?).and_return(false) - allow(commentable).to receive(:user_allowed_to_comment?).with(current_user).and_return(false) + allow(commentable).to receive(:user_allowed_to_comment?).with(current_user).and_return(true) end it "renders the comments blocked warning" do From 6d279791ecef9e46ae292cc6a0a0e4934ad9a7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A0xim=20Colls?= Date: Fri, 14 Jun 2024 16:34:49 +0200 Subject: [PATCH 09/19] Add Attachments with a Link (#12917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add link to Attachment and allow adding and updating the link from the admin panel * Move migration to decidim-participatory_processes engine * Add I18n and simplify dual display for Attachment with/without link * Normalise locales * Fix updating an attachments * Fix attachment update logic * Fix update_attachment specs * Hide download button for Attachment links * Update guidelines for Attachment Link tab * Add specific tests for attachment with_link * Add js to enable/disable the tabs in the attachment form * Add visit link button for link attachments * Update attachment content type for linked documents Co-authored-by: Andrés Pereira de Lucena * Add docs to file or link tabs js file * Fix tab panels cell inside forms * Rename attachments panel tab file * Add test for link attachment * Remove markup reference from js docs Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Eduardo Martinez Echevarria Co-authored-by: Andrés Pereira de Lucena --- .../decidim/admin/create_attachment.rb | 3 +- .../decidim/admin/update_attachment.rb | 11 ++-- .../forms/decidim/admin/attachment_form.rb | 8 ++- .../decidim/admin/attachments/_form.html.erb | 23 +++++++- .../decidim/admin/attachments/index.html.erb | 2 +- .../admin/test/manage_attachments_examples.rb | 36 ++++++++++++ .../decidim/admin/update_attachment_spec.rb | 2 + .../decidim/attachments_file_tab/show.erb | 3 + .../decidim/attachments_file_tab_cell.rb | 11 ++++ .../decidim/attachments_link_tab/show.erb | 12 ++++ .../decidim/attachments_link_tab_cell.rb | 21 +++++++ .../app/cells/decidim/tab_panels/show.erb | 2 +- .../app/cells/decidim/tab_panels_cell.rb | 12 ++++ decidim-core/app/models/decidim/attachment.rb | 28 +++++++++- .../decidim/attachments/file_or_link_tabs.js | 55 +++++++++++++++++++ .../packs/src/decidim/attachments/index.js | 1 + decidim-core/app/packs/src/decidim/index.js | 1 + .../stylesheets/decidim/_modal_update.scss | 2 +- .../packs/stylesheets/decidim/_tabs_x.scss | 6 +- .../decidim/application/_document.html.erb | 32 ++++++++--- decidim-core/config/locales/en.yml | 6 ++ decidim-core/lib/decidim/core/engine.rb | 1 + .../lib/decidim/core/test/factories.rb | 5 ++ .../has_attachment_collections.rb | 2 + .../test/shared_examples/has_attachments.rb | 5 +- .../spec/models/decidim/attachment_spec.rb | 44 +++++++++++++++ ...9161054_add_link_to_decidim_attachments.rb | 7 +++ 27 files changed, 316 insertions(+), 25 deletions(-) create mode 100644 decidim-core/app/cells/decidim/attachments_file_tab/show.erb create mode 100644 decidim-core/app/cells/decidim/attachments_file_tab_cell.rb create mode 100644 decidim-core/app/cells/decidim/attachments_link_tab/show.erb create mode 100644 decidim-core/app/cells/decidim/attachments_link_tab_cell.rb create mode 100644 decidim-core/app/packs/src/decidim/attachments/file_or_link_tabs.js create mode 100644 decidim-core/app/packs/src/decidim/attachments/index.js create mode 100644 decidim-participatory_processes/db/migrate/20240529161054_add_link_to_decidim_attachments.rb diff --git a/decidim-admin/app/commands/decidim/admin/create_attachment.rb b/decidim-admin/app/commands/decidim/admin/create_attachment.rb index cf26df4de6ce6..1a5c79d3d3a9f 100644 --- a/decidim-admin/app/commands/decidim/admin/create_attachment.rb +++ b/decidim-admin/app/commands/decidim/admin/create_attachment.rb @@ -51,7 +51,8 @@ def build_attachment weight: form.weight, attachment_collection: form.attachment_collection, file: form.file, # Define attached_to before this - content_type: blob(form.file).content_type + content_type: form.file && blob(form.file).content_type, + link: form.file ? nil : form.link ) end diff --git a/decidim-admin/app/commands/decidim/admin/update_attachment.rb b/decidim-admin/app/commands/decidim/admin/update_attachment.rb index bcd4c36f733d3..227ccffb413a3 100644 --- a/decidim-admin/app/commands/decidim/admin/update_attachment.rb +++ b/decidim-admin/app/commands/decidim/admin/update_attachment.rb @@ -44,13 +44,16 @@ def attributes { title: form.title, file: form.file, + link: form.link, description: form.description, - weight: form.weight, - attachment_collection: form.attachment_collection + weight: form.weight }.merge( attachment_attributes(:file) - ).reject do |attribute, value| - value.blank? && attribute != :attachment_collection + ).compact_blank.merge( + attachment_collection: form.attachment_collection + ).tap do |attrs| + attrs[:file] = nil if form.link.present? && form.file.blank? + attrs[:link] = nil if form.file.present? && form.link.blank? end end end diff --git a/decidim-admin/app/forms/decidim/admin/attachment_form.rb b/decidim-admin/app/forms/decidim/admin/attachment_form.rb index c590dbb91b49b..1a786a02af240 100644 --- a/decidim-admin/app/forms/decidim/admin/attachment_form.rb +++ b/decidim-admin/app/forms/decidim/admin/attachment_form.rb @@ -12,10 +12,12 @@ class AttachmentForm < Form translatable_attribute :description, String attribute :weight, Integer, default: 0 attribute :attachment_collection_id, Integer + attribute :link, String mimic :attachment - validates :file, presence: true, unless: :persisted? + validates :file, presence: true, unless: :persisted_or_link? + validates :link, url: true validates :file, passthru: { to: Decidim::Attachment } validates :title, :description, translatable_presence: true validates :attachment_collection, presence: true, if: ->(form) { form.attachment_collection_id.present? } @@ -25,6 +27,10 @@ class AttachmentForm < Form alias organization current_organization + def persisted_or_link? + persisted? || link.present? + end + def attachment_collections @attachment_collections ||= attached_to.attachment_collections end diff --git a/decidim-admin/app/views/decidim/admin/attachments/_form.html.erb b/decidim-admin/app/views/decidim/admin/attachments/_form.html.erb index 6c276e7b338c9..8d6fb8eca8df8 100644 --- a/decidim-admin/app/views/decidim/admin/attachments/_form.html.erb +++ b/decidim-admin/app/views/decidim/admin/attachments/_form.html.erb @@ -17,8 +17,27 @@ <%= form.select :attachment_collection_id, @form.attachment_collections.map { |c| [translated_attribute(c.name), c.id] }, include_blank: true %> -
- <%= form.upload :file, button_class: "button button__sm button__transparent-secondary" %> +
+ <%= cell "decidim/tab_panels", [ + { + enabled: true, + id: "file", + text: "Upload file", + icon: "file-upload-line", + method: :cell, + selected: form.object.file.present?, + args: ["/decidim/attachments_file_tab", form] + }, + { + enabled: true, + id: "link", + text: "Link", + icon: "link", + method: :cell, + selected: form.object.link.present?, + args: ["/decidim/attachments_link_tab", form] + } + ] %>
diff --git a/decidim-admin/app/views/decidim/admin/attachments/index.html.erb b/decidim-admin/app/views/decidim/admin/attachments/index.html.erb index fe9ae4896b8c0..5d8e77c70a676 100644 --- a/decidim-admin/app/views/decidim/admin/attachments/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/attachments/index.html.erb @@ -34,7 +34,7 @@ <%= attachment.file_type %> - <%= number_to_human_size(attachment.file_size) %> + <%= attachment.file? ? number_to_human_size(attachment.file_size) : "-" %> <% if allowed_to? :update, :attachment, attachment: attachment %> diff --git a/decidim-admin/lib/decidim/admin/test/manage_attachments_examples.rb b/decidim-admin/lib/decidim/admin/test/manage_attachments_examples.rb index 4f60bf6958036..9f5fe73e872b9 100644 --- a/decidim-admin/lib/decidim/admin/test/manage_attachments_examples.rb +++ b/decidim-admin/lib/decidim/admin/test/manage_attachments_examples.rb @@ -63,6 +63,42 @@ end end + it "can add attachments with a link to a process" do + click_on "New attachment" + + within ".new_attachment" do + fill_in_i18n( + :attachment_title, + "#attachment-title-tabs", + en: "Very Important Document", + es: "Documento Muy Importante", + ca: "Document Molt Important" + ) + + fill_in_i18n( + :attachment_description, + "#attachment-description-tabs", + en: "This document contains important information", + es: "Este documento contiene información importante", + ca: "Aquest document conté informació important" + ) + end + + within ".new_attachment" do + find_by_id("trigger-link").click + + fill_in "attachment[link]", with: "https://example.com/docs.pdf" + + find("*[type=submit]").click + end + + expect(page).to have_admin_callout("successfully") + + within "#attachments table" do + expect(page).to have_text("Very Important Document") + end + end + it "can add attachments within a collection to a process" do click_on "New attachment" diff --git a/decidim-admin/spec/commands/decidim/admin/update_attachment_spec.rb b/decidim-admin/spec/commands/decidim/admin/update_attachment_spec.rb index f2275f11f4d1a..be951322ab0f1 100644 --- a/decidim-admin/spec/commands/decidim/admin/update_attachment_spec.rb +++ b/decidim-admin/spec/commands/decidim/admin/update_attachment_spec.rb @@ -22,11 +22,13 @@ module Decidim::Admin es: "Una ciudad" }, file:, + link:, attachment_collection: nil, weight: 2 ) end let(:file) { upload_test_file(Decidim::Dev.test_file("city.jpeg", "image/jpeg")) } + let(:link) { "" } describe "when valid" do before do diff --git a/decidim-core/app/cells/decidim/attachments_file_tab/show.erb b/decidim-core/app/cells/decidim/attachments_file_tab/show.erb new file mode 100644 index 0000000000000..05bf98426f02e --- /dev/null +++ b/decidim-core/app/cells/decidim/attachments_file_tab/show.erb @@ -0,0 +1,3 @@ +
+ <%= form.upload :file, button_class: "button button__sm button__transparent-secondary" %> +
diff --git a/decidim-core/app/cells/decidim/attachments_file_tab_cell.rb b/decidim-core/app/cells/decidim/attachments_file_tab_cell.rb new file mode 100644 index 0000000000000..08f193427b314 --- /dev/null +++ b/decidim-core/app/cells/decidim/attachments_file_tab_cell.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Decidim + class AttachmentsFileTabCell < Decidim::ViewModel + alias form model + + def show + render :show + end + end +end diff --git a/decidim-core/app/cells/decidim/attachments_link_tab/show.erb b/decidim-core/app/cells/decidim/attachments_link_tab/show.erb new file mode 100644 index 0000000000000..9e3c0655ba393 --- /dev/null +++ b/decidim-core/app/cells/decidim/attachments_link_tab/show.erb @@ -0,0 +1,12 @@ +
+ <%= form.text_field :link, aria: { label: :url }, type: :text %> + +
+ <%= explanation %> +
    + <% help_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+
diff --git a/decidim-core/app/cells/decidim/attachments_link_tab_cell.rb b/decidim-core/app/cells/decidim/attachments_link_tab_cell.rb new file mode 100644 index 0000000000000..3dadc0f242e12 --- /dev/null +++ b/decidim-core/app/cells/decidim/attachments_link_tab_cell.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Decidim + class AttachmentsLinkTabCell < Decidim::ViewModel + alias form model + + def show + render :show + end + + private + + def explanation + I18n.t("decidim.forms.attachment_link.explanation") + end + + def help_messages + I18n.t("decidim.forms.attachment_link.help_messages") + end + end +end diff --git a/decidim-core/app/cells/decidim/tab_panels/show.erb b/decidim-core/app/cells/decidim/tab_panels/show.erb index 8dd1444e11614..77aa9a7eca1dd 100644 --- a/decidim-core/app/cells/decidim/tab_panels/show.erb +++ b/decidim-core/app/cells/decidim/tab_panels/show.erb @@ -3,7 +3,7 @@
    <% tabs.each_with_index do |tab, i| %>
  • -
  • diff --git a/decidim-core/app/cells/decidim/tab_panels_cell.rb b/decidim-core/app/cells/decidim/tab_panels_cell.rb index 2da6c8fd1e435..2f439af07367e 100644 --- a/decidim-core/app/cells/decidim/tab_panels_cell.rb +++ b/decidim-core/app/cells/decidim/tab_panels_cell.rb @@ -34,5 +34,17 @@ def tabs def panels @panels ||= items.map { |item| item.slice(:id, :method, :args) } end + + def selected_tab + @selected_tab ||= items.find { |item| item[:selected] } + end + + def selected?(tab, index) + if selected_tab + tab[:id] == selected_tab[:id] + else + index.zero? + end + end end end diff --git a/decidim-core/app/models/decidim/attachment.rb b/decidim-core/app/models/decidim/attachment.rb index 5d3d8c22d9345..4d4c58d6ef0b2 100644 --- a/decidim-core/app/models/decidim/attachment.rb +++ b/decidim-core/app/models/decidim/attachment.rb @@ -9,6 +9,7 @@ class Attachment < ApplicationRecord include Traceable before_save :set_content_type_and_size, if: :attached? + before_validation :set_link_content_type_and_size, if: :link? translatable_fields :title, :description belongs_to :attachment_collection, class_name: "Decidim::AttachmentCollection", optional: true @@ -61,15 +62,33 @@ def document? !photo? end + # Whether this attachment is a link or not. + # + # Returns Boolean. + def link? + link.present? + end + # Which kind of file this is. # # Returns String. def file_type - url&.split(".")&.last&.downcase + if file? + url&.split(".")&.last&.downcase + elsif link? + "link" + end end + # The URL that points to the attachment + # + # Returns String. def url - attached_uploader(:file).path + if file? + attached_uploader(:file).path + elsif link? + link + end end # The URL to download the thumbnail of the file. Only works with images. @@ -95,6 +114,11 @@ def set_content_type_and_size self.file_size = file.byte_size end + def set_link_content_type_and_size + self.content_type = "text/uri-list" + self.file_size = 0 + end + def self.log_presenter_class_for(_log) Decidim::AdminLog::AttachmentPresenter end diff --git a/decidim-core/app/packs/src/decidim/attachments/file_or_link_tabs.js b/decidim-core/app/packs/src/decidim/attachments/file_or_link_tabs.js new file mode 100644 index 0000000000000..23a7bbbbe951f --- /dev/null +++ b/decidim-core/app/packs/src/decidim/attachments/file_or_link_tabs.js @@ -0,0 +1,55 @@ +/** + * This file controls the behavior of the |File| and |Link| tabs in the + * attachment form. It disables the |File| tab when a link is present and + * vice versa. + */ + +const getFileButton = (container) => + container.querySelector("button#trigger-file"); +const getLinkButton = (container) => + container.querySelector("button#trigger-link"); +const getLinkInput = (container) => + container.querySelector("input#attachment_link"); +const getUploadsContainer = (container) => + container.querySelector("div[data-active-uploads]"); + +const hasUploads = (container) => container.querySelectorAll("div").length > 0; + +const updateTabsState = (container) => { + const fileButton = getFileButton(container); + const linkButton = getLinkButton(container); + const linkInput = getLinkInput(container); + const uploadsContainer = getUploadsContainer(container); + + const disableFileButton = Boolean(linkInput?.value); + const disableLinkButton = hasUploads(uploadsContainer); + + fileButton.disabled = disableFileButton; + linkButton.disabled = disableLinkButton; +}; + +const initializeTabs = (container) => { + const linkInput = getLinkInput(container); + const uploadsContainer = getUploadsContainer(container); + + linkInput.addEventListener("change", () => { + updateTabsState(container); + }); + + uploadsContainer.addEventListener("DOMSubtreeModified", () => { + updateTabsState(container); + console.log("DOMSubtreeModified"); + }); + + updateTabsState(container); +}; + +document.addEventListener("DOMContentLoaded", () => { + const tabs = document.querySelectorAll( + "div[data-file-or-link-tabs-controller]" + ); + + tabs.forEach((container) => { + initializeTabs(container); + }); +}); diff --git a/decidim-core/app/packs/src/decidim/attachments/index.js b/decidim-core/app/packs/src/decidim/attachments/index.js new file mode 100644 index 0000000000000..b0b86be4babc1 --- /dev/null +++ b/decidim-core/app/packs/src/decidim/attachments/index.js @@ -0,0 +1 @@ +import "src/decidim/attachments/file_or_link_tabs" diff --git a/decidim-core/app/packs/src/decidim/index.js b/decidim-core/app/packs/src/decidim/index.js index 6f9e7ae964792..5d00ad2a3ea80 100644 --- a/decidim-core/app/packs/src/decidim/index.js +++ b/decidim-core/app/packs/src/decidim/index.js @@ -50,6 +50,7 @@ import "src/decidim/data_consent" import "src/decidim/abide_form_validator_fixer" import "src/decidim/sw" import "src/decidim/sticky_header" +import "src/decidim/attachments" // local deps that require initialization import formDatePicker from "src/decidim/datepicker/form_datepicker" diff --git a/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss b/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss index 87a8a982e4593..f5e0b50d70b04 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_modal_update.scss @@ -7,7 +7,7 @@ } &-container { - @apply mt-4 md:mt-12 space-y-4; + @apply space-y-4; } &-placeholder { diff --git a/decidim-core/app/packs/stylesheets/decidim/_tabs_x.scss b/decidim-core/app/packs/stylesheets/decidim/_tabs_x.scss index bb5f748f64093..3a9ec688f4aa6 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_tabs_x.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_tabs_x.scss @@ -6,13 +6,17 @@ } &[aria-expanded="true"], - &:hover { + &:hover[disabled="false"] { @apply text-secondary border-secondary; svg { @apply text-secondary; } } + + &[disabled] { + @apply opacity-50 cursor-not-allowed; + } } .tabs-1 { diff --git a/decidim-core/app/views/decidim/application/_document.html.erb b/decidim-core/app/views/decidim/application/_document.html.erb index a7f386f0f9cd0..1b2007867122b 100644 --- a/decidim-core/app/views/decidim/application/_document.html.erb +++ b/decidim-core/app/views/decidim/application/_document.html.erb @@ -8,16 +8,30 @@ <% end %> - <%= link_to document.url, - target: "_blank", - rel: "noopener noreferrer", - class: "button button__sm button__transparent-secondary flex-none", - title: t("decidim.application.document.download"), - data: { "external-link": false } do %> - <%= t("decidim.application.document.download") %> - <%= icon "download-line" %> + <% if document.file? %> + <%= link_to document.url, + target: "_blank", + rel: "noopener noreferrer", + class: "button button__sm button__transparent-secondary flex-none", + title: t("decidim.application.document.download"), + data: { "external-link": false } do %> + <%= t("decidim.application.document.download") %> + <%= icon "download-line" %> + <% end %> + <% else %> + <%= link_to document.url, + target: "_blank", + rel: "noopener noreferrer", + class: "button button__sm button__transparent-secondary flex-none", + title: t("decidim.application.document.visit_link"), + data: { "external-link": true } do %> + <%= t("decidim.application.document.visit_link") %> + <% end %> + <% end %> diff --git a/decidim-core/config/locales/en.yml b/decidim-core/config/locales/en.yml index 8f7e073bc6040..8421b63b3ec38 100644 --- a/decidim-core/config/locales/en.yml +++ b/decidim-core/config/locales/en.yml @@ -360,6 +360,7 @@ en: application: document: download: Download file + visit_link: Visit link documents: component_documents: dummy: Dummy component documents @@ -887,6 +888,11 @@ en: button: Stop following error: There was a problem unfollowing this resource. forms: + attachment_link: + explanation: Guidance for attachment links + help_messages: + - Only correct URL formats are accepted. + - Please note that the links must be public so that all participants can access them. errors: decidim/user: password: The password is too short. diff --git a/decidim-core/lib/decidim/core/engine.rb b/decidim-core/lib/decidim/core/engine.rb index 1aed6cd63a180..01f0e0184ded8 100644 --- a/decidim-core/lib/decidim/core/engine.rb +++ b/decidim-core/lib/decidim/core/engine.rb @@ -156,6 +156,7 @@ class Engine < ::Rails::Engine # Attachments Decidim.icons.register(name: "file-text-line", icon: "file-text-line", category: "system", description: "", engine: :core) + Decidim.icons.register(name: "file-upload-line", icon: "file-upload-line", category: "documents", description: "File upload", engine: :core) Decidim.icons.register(name: "scales-2-line", icon: "scales-2-line", category: "system", description: "", engine: :core) Decidim.icons.register(name: "image-line", icon: "image-line", category: "system", description: "", engine: :core) Decidim.icons.register(name: "error-warning-line", icon: "error-warning-line", category: "system", description: "", engine: :core) diff --git a/decidim-core/lib/decidim/core/test/factories.rb b/decidim-core/lib/decidim/core/test/factories.rb index 0c735187ea13c..a67144c0f5c7a 100644 --- a/decidim-core/lib/decidim/core/test/factories.rb +++ b/decidim-core/lib/decidim/core/test/factories.rb @@ -473,6 +473,11 @@ def generate_localized_title(field = nil, skip_injection: false) content_type { "application/pdf" } file_size { 17_525 } end + + trait :with_link do + file { nil } + link { Faker::Internet.url } + end end factory :component, class: "Decidim::Component" do diff --git a/decidim-core/lib/decidim/core/test/shared_examples/has_attachment_collections.rb b/decidim-core/lib/decidim/core/test/shared_examples/has_attachment_collections.rb index cb0da39415472..d519d3550f697 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/has_attachment_collections.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/has_attachment_collections.rb @@ -6,6 +6,7 @@ context "when it has attachment collections" do let(:attachment_collection) { create(:attachment_collection, collection_for:) } let!(:document) { create(:attachment, :with_pdf, attached_to:, attachment_collection:) } + let!(:link) { create(:attachment, :with_link, attached_to:, attachment_collection:) } let!(:other_document) { create(:attachment, :with_pdf, attached_to:, attachment_collection: nil) } before do @@ -19,6 +20,7 @@ it "show their documents" do within "[id*=documents-#{attachment_collection.id}]", visible: false do expect(page).to have_content(:all, translated(document.title)) + expect(page).to have_content(:all, translated(link.title)) expect(page).to have_no_content(:all, translated(other_document.title)) end end diff --git a/decidim-core/lib/decidim/core/test/shared_examples/has_attachments.rb b/decidim-core/lib/decidim/core/test/shared_examples/has_attachments.rb index 631d6d7a302c2..5f488084ed0ad 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/has_attachments.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/has_attachments.rb @@ -48,7 +48,7 @@ shared_examples_for "has attachments tabs" do context "when it has attachments" do let!(:document) { create(:attachment, :with_pdf, attached_to:) } - + let!(:link) { create(:attachment, :with_link, attached_to:) } let!(:image) { create(:attachment, attached_to:) } before do @@ -59,6 +59,7 @@ find("li [data-controls='panel-documents']").click within "#panel-documents" do expect(page).to have_content(translated(document.title)) + expect(page).to have_content(translated(link.title)) end find("li [data-controls='panel-images']").click @@ -69,7 +70,7 @@ end context "when are ordered by weight" do - let!(:last_document) { create(:attachment, :with_pdf, attached_to:, weight: 2) } + let!(:last_document) { create(:attachment, :with_link, attached_to:, weight: 2) } let!(:first_document) { create(:attachment, :with_pdf, attached_to:, weight: 1) } let!(:last_image) { create(:attachment, attached_to:, weight: 2) } let!(:first_image) { create(:attachment, attached_to:, weight: 1) } diff --git a/decidim-core/spec/models/decidim/attachment_spec.rb b/decidim-core/spec/models/decidim/attachment_spec.rb index dfd3f09fe3f19..c756bdf3650e7 100644 --- a/decidim-core/spec/models/decidim/attachment_spec.rb +++ b/decidim-core/spec/models/decidim/attachment_spec.rb @@ -6,6 +6,12 @@ module Decidim describe Attachment do subject { build(:attachment) } + RSpec::Matchers.define :be_url do |_expected| + match do |actual| + actual =~ URI::DEFAULT_PARSER.make_regexp + end + end + let(:organization) { subject.organization } it { is_expected.to be_valid } @@ -76,6 +82,12 @@ module Decidim end end + describe "link?" do + it "returns false" do + expect(subject.link?).to be(false) + end + end + describe "photo?" do it "returns true" do expect(subject.photo?).to be(true) @@ -100,6 +112,38 @@ module Decidim expect(subject.big_url).to be_nil end + describe "link?" do + it "returns false" do + expect(subject.link?).to be(false) + end + end + + describe "photo?" do + it "returns false" do + expect(subject.photo?).to be(false) + end + end + + describe "document?" do + it "returns true" do + expect(subject.document?).to be(true) + end + end + end + + context "when it has a link" do + subject { build(:attachment, :with_link) } + + it "has a correct link url" do + expect(subject.link).to be_url + end + + describe "link?" do + it "returns true" do + expect(subject.link?).to be(true) + end + end + describe "photo?" do it "returns false" do expect(subject.photo?).to be(false) diff --git a/decidim-participatory_processes/db/migrate/20240529161054_add_link_to_decidim_attachments.rb b/decidim-participatory_processes/db/migrate/20240529161054_add_link_to_decidim_attachments.rb new file mode 100644 index 0000000000000..e744b1497a2c6 --- /dev/null +++ b/decidim-participatory_processes/db/migrate/20240529161054_add_link_to_decidim_attachments.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddLinkToDecidimAttachments < ActiveRecord::Migration[6.1] + def change + add_column :decidim_attachments, :link, :string + end +end From 9e284cd88e15ada15697799d14a52a5d9ed3bf6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Sun, 16 Jun 2024 10:31:51 +0200 Subject: [PATCH 10/19] Do not allow registering to a meeting if it started (#12995) * Do not allow registering to a meeting if it started * Fix spec * Add spec with the behavior change * Fix rubocop offense --- .../app/models/decidim/meetings/meeting.rb | 6 +++++- .../spec/system/meeting_registrations_spec.rb | 12 ++++++++++++ .../spec/system/user_creates_meeting_spec.rb | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/decidim-meetings/app/models/decidim/meetings/meeting.rb b/decidim-meetings/app/models/decidim/meetings/meeting.rb index 22001216f0405..99b6f02d613e2 100644 --- a/decidim-meetings/app/models/decidim/meetings/meeting.rb +++ b/decidim-meetings/app/models/decidim/meetings/meeting.rb @@ -171,7 +171,7 @@ def self.log_presenter_class_for(_log) end def can_be_joined_by?(user) - !closed? && registrations_enabled? && can_participate?(user) + !started? && registrations_enabled? && can_participate?(user) end def can_register_invitation?(user) @@ -183,6 +183,10 @@ def closed? closed_at.present? end + def started? + start_time < Time.current + end + def past? end_time < Time.current end diff --git a/decidim-meetings/spec/system/meeting_registrations_spec.rb b/decidim-meetings/spec/system/meeting_registrations_spec.rb index 90cd57f1f94ae..e6035ec03cab7 100644 --- a/decidim-meetings/spec/system/meeting_registrations_spec.rb +++ b/decidim-meetings/spec/system/meeting_registrations_spec.rb @@ -156,6 +156,18 @@ def questionnaire_public_path login_as user, scope: :user end + context "and the meeting is happening now" do + before do + meeting.update!(start_time: 1.hour.ago, end_time: 1.hour.from_now) + end + + it "does not show the registration button" do + visit_meeting + + expect(page).to have_no_css(".button", text: "Register") + end + end + context "and they ARE NOT part of a verified user group" do it "they can join the meeting and automatically follow it" do visit_meeting diff --git a/decidim-meetings/spec/system/user_creates_meeting_spec.rb b/decidim-meetings/spec/system/user_creates_meeting_spec.rb index 80b7fbbcefe85..4305479514216 100644 --- a/decidim-meetings/spec/system/user_creates_meeting_spec.rb +++ b/decidim-meetings/spec/system/user_creates_meeting_spec.rb @@ -228,7 +228,7 @@ expect(page).to have_content(meeting_address) expect(page).to have_content(meeting_start_time) expect(page).to have_content(meeting_end_time) - expect(page).to have_css(".button", text: "Register") + expect(page).to have_no_css(".button", text: "Register") expect(page).to have_css("[data-author]", text: user_group.name) end end From f390ba54bf79dfc52eff0f4dca3f002468ac4cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Sun, 16 Jun 2024 12:17:26 +0200 Subject: [PATCH 11/19] Add checks in system for the secret key and the ActiveJob queue (#12846) * Add a check in system for the secret key * Add a check in system for the ActiveJob queue * Refactor the UI to keep it DRY * Fix a bug when the secret_key is nil * Open external URL in a new tab Suggested in code review * Use remixicon from redesign in system panel * Fix rubocop offense * Fix erblint offense --- .../decidim/system/system_checks/show.erb | 13 +++ .../decidim/system/system_checks_cell.rb | 48 +++++++++++ .../decidim/system/dashboard_controller.rb | 2 + .../decidim/system/dashboard/show.html.erb | 4 + .../decidim/system/_js_configuration.html.erb | 2 +- decidim-system/config/locales/en.yml | 9 +++ decidim-system/lib/decidim/system/engine.rb | 4 + .../decidim/system/system_checks_cell_spec.rb | 80 +++++++++++++++++++ 8 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 decidim-system/app/cells/decidim/system/system_checks/show.erb create mode 100644 decidim-system/app/cells/decidim/system/system_checks_cell.rb create mode 100644 decidim-system/spec/cells/decidim/system/system_checks_cell_spec.rb diff --git a/decidim-system/app/cells/decidim/system/system_checks/show.erb b/decidim-system/app/cells/decidim/system/system_checks/show.erb new file mode 100644 index 0000000000000..197264438f930 --- /dev/null +++ b/decidim-system/app/cells/decidim/system/system_checks/show.erb @@ -0,0 +1,13 @@ +
      + <% checks.each do |key, check| %> +
    • + <% if check[:check_method] %> + <%= icon "checkbox-circle-line", class: "fill-success inline" %> + <%= t("#{key}.success", scope: "decidim.system.system_checks") %> + <% else %> + <%= icon "close-circle-line", class: "fill-alert inline" %> + <%= t("#{key}.error", scope: "decidim.system.system_checks", error_extra: check[:error_extra]) %> + <% end %> +
    • + <% end %> +
    diff --git a/decidim-system/app/cells/decidim/system/system_checks_cell.rb b/decidim-system/app/cells/decidim/system/system_checks_cell.rb new file mode 100644 index 0000000000000..658c891248d16 --- /dev/null +++ b/decidim-system/app/cells/decidim/system/system_checks_cell.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Decidim + module System + class SystemChecksCell < Decidim::ViewModel + def show + render + end + + private + + def checks + { + secret_key: { + check_method: correct_secret_key_base?, + error_extra: generated_secret_key + }, + active_job_queue: { + check_method: correct_active_job_queue?, + error_extra: active_job_queue_link + } + } + end + + def correct_secret_key_base? + Rails.application.secrets.secret_key_base&.length == 128 + end + + def generated_secret_key + SecureRandom.hex(64) + end + + def correct_active_job_queue? + # The default ActiveJob queue is not recommended for production environments, + # as it can lose jobs when restarting + Rails.application.config.active_job.queue_adapter != :async + end + + def active_job_queue_link + link_to(t("active_job_queue.decidim_documentation", scope: "decidim.system.system_checks"), + "https://docs.decidim.org/en/develop/services/activejob", + class: "underline text-primary", + target: "_blank", + rel: "nofollow noopener noreferrer") + end + end + end +end diff --git a/decidim-system/app/controllers/decidim/system/dashboard_controller.rb b/decidim-system/app/controllers/decidim/system/dashboard_controller.rb index f751fde71333a..022c30dae1deb 100644 --- a/decidim-system/app/controllers/decidim/system/dashboard_controller.rb +++ b/decidim-system/app/controllers/decidim/system/dashboard_controller.rb @@ -10,6 +10,8 @@ def show @admins = Admin.all end + private + def check_organizations_presence return if Organization.exists? diff --git a/decidim-system/app/views/decidim/system/dashboard/show.html.erb b/decidim-system/app/views/decidim/system/dashboard/show.html.erb index 560897e702c52..d5bd056742fc9 100644 --- a/decidim-system/app/views/decidim/system/dashboard/show.html.erb +++ b/decidim-system/app/views/decidim/system/dashboard/show.html.erb @@ -4,6 +4,10 @@

    <%= t("decidim.system.titles.dashboard") %>

    <% end %> +

    <%= t ".system_checks" %>

    + +<%= cell "decidim/system/system_checks", nil %> +

    <%= t ".current_organizations" %>

    <%= link_to t("actions.new_organization", scope: "decidim.system"), [:new, :organization], class: "button button__sm md:button__lg button__primary" %> diff --git a/decidim-system/app/views/layouts/decidim/system/_js_configuration.html.erb b/decidim-system/app/views/layouts/decidim/system/_js_configuration.html.erb index c226c5c5b16eb..626b5148f1050 100644 --- a/decidim-system/app/views/layouts/decidim/system/_js_configuration.html.erb +++ b/decidim-system/app/views/layouts/decidim/system/_js_configuration.html.erb @@ -1,6 +1,6 @@ <% js_configs = { - icons_path: Decidim.cors_enabled ? "" : asset_pack_path("media/images/icons.svg") + icons_path: Decidim.cors_enabled ? "" : asset_pack_path("media/images/remixicon.symbol.svg") } %> diff --git a/decidim-system/config/locales/en.yml b/decidim-system/config/locales/en.yml index f030e881c52d8..5a395cf918ca9 100644 --- a/decidim-system/config/locales/en.yml +++ b/decidim-system/config/locales/en.yml @@ -68,6 +68,7 @@ en: show: admins: Admins current_organizations: Current organizations + system_checks: System checks default_pages: placeholders: content: Please add meaningful content to the %{page} static page on the admin dashboard. @@ -269,6 +270,14 @@ en: organizations_list: confirm_resend_invitation: Are you sure you want to resend the invitation? resend_invitation: Resend invitation + system_checks: + active_job_queue: + decidim_documentation: Decidim Documentation + error: The ActiveJob queue is not configured. This is not a recommended setup for production. Read more at %{error_extra}. + success: The ActiveJob queue is configured correctly. + secret_key: + error: 'The secret key is not defined correctly. Please save to the SECRET_KEY_BASE environment variable and restart the server: %{error_extra}' + success: The secret key is configured correctly. titles: dashboard: Dashboard decidim: Decidim diff --git a/decidim-system/lib/decidim/system/engine.rb b/decidim-system/lib/decidim/system/engine.rb index 84a38f5f808b0..c6d22d91faf4e 100644 --- a/decidim-system/lib/decidim/system/engine.rb +++ b/decidim-system/lib/decidim/system/engine.rb @@ -28,6 +28,10 @@ class Engine < ::Rails::Engine initializer "decidim_system.webpacker.assets_path" do Decidim.register_assets_path File.expand_path("app/packs", root) end + + initializer "decidim_system.add_cells_view_paths" do + Cell::ViewModel.view_paths << File.expand_path("#{Decidim::System::Engine.root}/app/cells") + end end end end diff --git a/decidim-system/spec/cells/decidim/system/system_checks_cell_spec.rb b/decidim-system/spec/cells/decidim/system/system_checks_cell_spec.rb new file mode 100644 index 0000000000000..8cc9b3a693df2 --- /dev/null +++ b/decidim-system/spec/cells/decidim/system/system_checks_cell_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Decidim::System::SystemChecksCell, type: :cell do + controller Decidim::System::DashboardController + + subject { my_cell.call(:show) } + + let(:my_cell) { cell("decidim/system/system_checks", nil) } + + describe "#generated_secret_key" do + it "generates it well" do + generated_secret_key = my_cell.send(:generated_secret_key) + expect(generated_secret_key.length).to eq 128 + end + + it "does not generates two times the same string" do + generated_secret_key1 = my_cell.send(:generated_secret_key) + generated_secret_key2 = my_cell.send(:generated_secret_key) + expect(generated_secret_key1).not_to eq generated_secret_key2 + end + end + + describe "secret_key_check" do + before do + allow(Rails.application.secrets).to receive(:secret_key_base).and_return(secret_key) + end + + context "when the secret key is correct" do + let(:secret_key) { "98a143987c91e79d9b587c65f720c030a91131dd70427a305706d9d6652b8e97d1498b1dd329669edfc9302d03039303425732cdb3c1ee8429e2d58dee179c55" } + + it "shows the success message" do + expect(subject).to have_content "The secret key is configured correctly" + end + end + + context "when the secret key is empty" do + let(:secret_key) { "" } + + it "shows the error message" do + expect(subject).to have_content "The secret key is not defined correctly" + expect(subject).to have_content "Please save to the SECRET_KEY_BASE environment variable and restart the server" + end + end + + context "when the secret key is nil" do + let(:secret_key) { nil } + + it "shows the error message" do + expect(subject).to have_content "The secret key is not defined correctly" + expect(subject).to have_content "Please save to the SECRET_KEY_BASE environment variable and restart the server" + end + end + end + + describe "active_job_queue_check" do + before do + allow(Rails.application.config.active_job).to receive(:queue_adapter).and_return(active_job_queue) + end + + context "when the ActiveJob queue is correct" do + let(:active_job_queue) { :sidekiq } + + it "shows the success message" do + expect(subject).to have_content "The ActiveJob queue is configured correctly" + end + end + + context "when the ActiveJob queue is incorrect" do + let(:active_job_queue) { :async } + + it "shows the error message" do + expect(subject).to have_content "The ActiveJob queue is not configured." + expect(subject).to have_content "This is not a recommended setup for production" + expect(subject).to have_link("Decidim Documentation", href: "https://docs.decidim.org/en/develop/services/activejob") + end + end + end +end From 928272d8755a1c4fd1f8c879370e8d53305be5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Sun, 16 Jun 2024 12:23:51 +0200 Subject: [PATCH 12/19] Change default proposal sorting word to automatic (#12984) * Change default proposal sorting word to automatic * Fix specs Suggested in code review --- .../controllers/concerns/decidim/proposals/orderable.rb | 2 +- decidim-proposals/config/locales/en.yml | 8 ++++---- decidim-proposals/lib/decidim/proposals/component.rb | 4 ++-- .../spec/controllers/concerns/orderable_spec.rb | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb b/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb index fc13ef5f9069c..99d11be89e7cc 100644 --- a/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb +++ b/decidim-proposals/app/controllers/concerns/decidim/proposals/orderable.rb @@ -35,7 +35,7 @@ def default_order def fetch_default_order default_order = current_settings.default_sort_order.presence || component_settings.default_sort_order - return order_by_default if default_order == "default" + return order_by_default if default_order == "automatic" possible_orders.include?(default_order) ? default_order : order_by_default end diff --git a/decidim-proposals/config/locales/en.yml b/decidim-proposals/config/locales/en.yml index 4c3d32cac16e5..78dcc3cc776bf 100644 --- a/decidim-proposals/config/locales/en.yml +++ b/decidim-proposals/config/locales/en.yml @@ -161,9 +161,9 @@ en: comments_enabled: Comments enabled comments_max_length: Comments max length (Leave 0 for default value) default_sort_order: Default proposal sorting - default_sort_order_help: Default means that if the votes are enabled, the proposals will be shown sorted by random, and if the votes are blocked, then they will be sorted by the most voted. + default_sort_order_help: Automatic means that if the votes are enabled, the proposals will be shown sorted by random, and if the votes are blocked, then they will be sorted by the most voted. default_sort_order_options: - default: Default + automatic: Automatic most_commented: Most commented most_endorsed: Most endorsed most_followed: Most followed @@ -213,9 +213,9 @@ en: creation_enabled: Participants can create proposals creation_enabled_readonly: This setting is disabled when you activate the Participatory Texts functionality. To upload proposals as participatory text click on the Participatory Texts button and follow the instructions. default_sort_order: Default proposal sorting - default_sort_order_help: Default it means that if the votes are enabled, the proposals will be shown sorted by random, and if the votes are blocked, then they will be sorted by the most voted. + default_sort_order_help: Automatic means that if the votes are enabled, the proposals will be shown sorted by random, and if the votes are blocked, then they will be sorted by the most voted. default_sort_order_options: - default: Default + automatic: Automatic most_commented: Most commented most_endorsed: Most endorsed most_followed: Most followed diff --git a/decidim-proposals/lib/decidim/proposals/component.rb b/decidim-proposals/lib/decidim/proposals/component.rb index 05eb845f567ae..427f795346491 100644 --- a/decidim-proposals/lib/decidim/proposals/component.rb +++ b/decidim-proposals/lib/decidim/proposals/component.rb @@ -28,7 +28,7 @@ component.permissions_class_name = "Decidim::Proposals::Permissions" - POSSIBLE_SORT_ORDERS = %w(default random recent most_endorsed most_voted most_commented most_followed with_more_authors).freeze + POSSIBLE_SORT_ORDERS = %w(automatic random recent most_endorsed most_voted most_commented most_followed with_more_authors).freeze component.settings(:global) do |settings| settings.attribute :scopes_enabled, type: :boolean, default: false @@ -42,7 +42,7 @@ settings.attribute :threshold_per_proposal, type: :integer, default: 0, required: true settings.attribute :can_accumulate_votes_beyond_threshold, type: :boolean, default: false settings.attribute :proposal_answering_enabled, type: :boolean, default: true - settings.attribute :default_sort_order, type: :select, default: "default", choices: -> { POSSIBLE_SORT_ORDERS } + settings.attribute :default_sort_order, type: :select, default: "automatic", choices: -> { POSSIBLE_SORT_ORDERS } settings.attribute :official_proposals_enabled, type: :boolean, default: true settings.attribute :comments_enabled, type: :boolean, default: true settings.attribute :comments_max_length, type: :integer, required: true diff --git a/decidim-proposals/spec/controllers/concerns/orderable_spec.rb b/decidim-proposals/spec/controllers/concerns/orderable_spec.rb index 20965bcdc2795..a7368d1e5f5a0 100644 --- a/decidim-proposals/spec/controllers/concerns/orderable_spec.rb +++ b/decidim-proposals/spec/controllers/concerns/orderable_spec.rb @@ -27,7 +27,7 @@ class OrderableFakeController < Decidim::ApplicationController endorsements_enabled?: endorsements_enabled, comments_enabled?: comments_enabled) end - let(:component_default_sort_order) { "default" } + let(:component_default_sort_order) { "automatic" } let(:step_default_sort_order) { "" } let(:votes_enabled) { nil } let(:votes_blocked) { nil } @@ -64,7 +64,7 @@ class OrderableFakeController < Decidim::ApplicationController context "when step has default default_sort_order" do let(:component_default_sort_order) { "most_followed" } - let(:step_default_sort_order) { "default" } + let(:step_default_sort_order) { "automatic" } let(:votes_blocked) { false } it "use it instead of component's" do @@ -91,7 +91,7 @@ class OrderableFakeController < Decidim::ApplicationController end describe "by default" do - let(:default_sort_order) { "default" } + let(:default_sort_order) { "automatic" } it "default_order is random" do expect(controller.send(:default_order)).to eq("random") From 761ae2cee1ce650212fdcf9f03f355c7da4dd115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Mart=C3=ADnez?= Date: Mon, 17 Jun 2024 08:00:38 +0200 Subject: [PATCH 13/19] Extend polls feature in meetings (#12957) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Define resource permission of meetings to reply polls * Add poll answers view including polling behavior * Add back to meeting link * Add permission to update polls questions in front * Add endpoint to admin questions in polls * Disable reply poll button if poll has no questions * Add unanswered scope to questions * Remove polls from live event page * Move reply poll button * Change editable method of questionnaire and define one for question form * Do not delete answers after updating questionnaire * Prevent update of questions not editable * Allow edition of questions of questionnaire * Change class of not editable questions to avoid js updates * Add announcement to poll edition in admin * Add locked icon to not editable questions * desktop version * Allow edition of questionnaire questions always * Allow replying meeting polls when questions are present * mobile version * fix styles * Update titles * fix label_tag * Simplify single option form * Change class of not editable questions max choices to avoid js updates * Allow changing position of not editable questions of questionnaire * Provide all associated values of not editable questions in forms to avoid blank data if validation fails * Add comment * Update polling feature * Recover questionStatuses storage * Simplify multiple_option answers interface * Add max choices to validation error message * Provide edit in admin button on questions unpublished from administration page and display status * Include a link from admin to administrate poll questions * Update button * Change announcement content * Add a button to administrate poll in meeting aside if admin is not allowed to reply * Use action_authorized_link_to in reply poll from administrate page * Fix linter offenses * Remove class in use from list of deprecated * Remove unused translation * Add questions statuses translations to ignore unused list * Add missing quotes * Change poll button text when all questions are closed * Remove useless method The value of this method has been set to true always so it's not required * Open admin edition of poll in new window from poll administrate * Adapt test to administrate poll questions * Adapt test to reply poll questions * Remove unused factory * Add sleep in example * Allow reordering not editable questions in admin * Update admin manages meetings in polls examples * Improve tests of reply poll * Add examples to check the status display of questions in administrate * Display poll actions buttons in meeting show in different place depending on mobile * Add sleep * Memoize method * Change selector to display validation error message * Update id of multiple choices to avoid duplications * Change title of questions to include max choices data if present * Update config/i18n-tasks.yml Co-authored-by: Andrés Pereira de Lucena * Remove reference to polls in live event meeting embed type description * Replace selectors to use data attributes --------- Co-authored-by: Hugoren Martinako Co-authored-by: Andrés Pereira de Lucena --- .erb-lint.yml | 1 - config/i18n-tasks.yml | 1 + .../app/helpers/decidim/application_helper.rb | 4 + .../layouts/decidim/_application.html.erb | 2 +- .../app/forms/decidim/forms/answer_form.rb | 4 +- decidim-forms/config/locales/en.yml | 2 +- .../meetings/question_responses/show.erb | 10 +- .../meetings/admin/update_questionnaire.rb | 26 ++-- .../decidim/meetings/create_answer.rb | 2 +- .../admin/meetings_poll_controller.rb | 12 ++ .../meetings/polls/answers_controller.rb | 8 ++ .../decidim/meetings/admin/question_form.rb | 12 +- .../app/forms/decidim/meetings/answer_form.rb | 8 +- .../app/models/decidim/meetings/poll.rb | 8 ++ .../app/models/decidim/meetings/question.rb | 1 + .../models/decidim/meetings/questionnaire.rb | 6 - .../src/decidim/meetings/meetings_polls.js | 5 + .../src/decidim/meetings/poll.component.js | 26 +++- .../stylesheets/decidim/meetings/_item.scss | 104 ++++++++++++++ .../decidim/meetings/_live_event.scss | 94 ------------- .../decidim/meetings/permissions.rb | 20 +++ .../poll/_answer_option_template.html.erb | 2 +- .../meetings/admin/poll/_form.html.erb | 51 +++---- .../meetings/admin/poll/_question.html.erb | 30 ++-- .../decidim/meetings/admin/poll/edit.html.erb | 10 +- .../meetings/layouts/live_event.html.erb | 14 -- .../meetings/meetings/_meeting.html.erb | 1 + .../meetings/meetings/_meeting_aside.html.erb | 2 + .../meetings/_meeting_poll_actions.html.erb | 15 ++ .../polls/answers/_multiple_option.html.erb | 16 +-- .../polls/answers/_single_option.html.erb | 14 +- .../meetings/polls/answers/admin.html.erb | 34 +++++ .../meetings/polls/answers/index.html.erb | 36 +++++ .../polls/questions/_closed_question.html.erb | 10 +- .../polls/questions/_index_admin.html.erb | 51 +++---- .../questions/_published_question.html.erb | 25 ++-- .../polls/questions/_question.html.erb | 2 +- .../meetings/polls/questions/index.js.erb | 6 +- .../polls/questions/index_admin.js.erb | 6 +- decidim-meetings/config/locales/en.yml | 31 +++- .../lib/decidim/meetings/component.rb | 2 +- .../lib/decidim/meetings/engine.rb | 6 +- .../decidim/meetings/questionnaire_spec.rb | 7 - .../admin_manages_meetings_polls_spec.rb | 133 +++++++++++++++++- ...=> meeting_questions_administrate_spec.rb} | 45 ++++-- ...b => meeting_questions_reply_poll_spec.rb} | 51 ++++--- 46 files changed, 651 insertions(+), 305 deletions(-) create mode 100644 decidim-meetings/app/views/decidim/meetings/meetings/_meeting_poll_actions.html.erb create mode 100644 decidim-meetings/app/views/decidim/meetings/polls/answers/admin.html.erb create mode 100644 decidim-meetings/app/views/decidim/meetings/polls/answers/index.html.erb rename decidim-meetings/spec/system/{live_meeting_admin_questions_spec.rb => meeting_questions_administrate_spec.rb} (79%) rename decidim-meetings/spec/system/{live_meeting_answer_questions_spec.rb => meeting_questions_reply_poll_spec.rb} (77%) diff --git a/.erb-lint.yml b/.erb-lint.yml index 9c0b1a2e65704..b892e89a52894 100644 --- a/.erb-lint.yml +++ b/.erb-lint.yml @@ -701,7 +701,6 @@ linters: - is-accordion-submenu - is-accordion-submenu-parent - is-accordion-submenu-parent[aria-expanded=true] - - is-admin - is-anchored - is-at-bottom - is-at-top diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 74699fcd30c91..4b07123b5628f 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -350,6 +350,7 @@ ignore_unused: - layouts.decidim.assemblies.promoted_assembly.take_part - versions.dropdown.option_* - decidim.meetings.meetings.filters.* + - decidim.meetings.polls.questions.index_admin.statuses.{closed,published,unpublished} - decidim.meetings.directory.meetings.index.space_type - decidim.authorization_modals.content.* - time.buttons.* diff --git a/decidim-core/app/helpers/decidim/application_helper.rb b/decidim-core/app/helpers/decidim/application_helper.rb index 3ab0601d4f43e..29ee5ca911078 100644 --- a/decidim-core/app/helpers/decidim/application_helper.rb +++ b/decidim-core/app/helpers/decidim/application_helper.rb @@ -109,5 +109,9 @@ def prevent_timeout_seconds def text_initials(name) name.split(/[\s.]+/).map(&:chr).slice(0, 2).join.upcase end + + def add_body_classes(*class_names) + content_for :body_class, class_names.map { |class_name| " #{class_name.strip}" }.join + end end end diff --git a/decidim-core/app/views/layouts/decidim/_application.html.erb b/decidim-core/app/views/layouts/decidim/_application.html.erb index 673502a4b9dc0..23d5819d3210a 100644 --- a/decidim-core/app/views/layouts/decidim/_application.html.erb +++ b/decidim-core/app/views/layouts/decidim/_application.html.erb @@ -6,7 +6,7 @@ <%= render partial: "layouts/decidim/head" %> - + <%= link_to t("skip_button", scope: "decidim.accessibility"), "#content", class: "layout-container__skip" %> <%= cell("decidim/data_consent", current_organization) %> diff --git a/decidim-forms/app/forms/decidim/forms/answer_form.rb b/decidim-forms/app/forms/decidim/forms/answer_form.rb index 6bc50204a3af9..bdefe0f8a43d2 100644 --- a/decidim-forms/app/forms/decidim/forms/answer_form.rb +++ b/decidim-forms/app/forms/decidim/forms/answer_form.rb @@ -108,9 +108,9 @@ def grouped_choices def max_choices if matrix? - errors.add(:choices, :too_many) if grouped_choices.any? { |choices| choices.count > question.max_choices } + errors.add(:choices, :too_many, count: question.max_choices) if grouped_choices.any? { |choices| choices.count > question.max_choices } elsif selected_choices.size > question.max_choices - errors.add(:choices, :too_many) + errors.add(:choices, :too_many, count: question.max_choices) end end diff --git a/decidim-forms/config/locales/en.yml b/decidim-forms/config/locales/en.yml index 8690cb53fc5e8..2f59f12d0fb1a 100644 --- a/decidim-forms/config/locales/en.yml +++ b/decidim-forms/config/locales/en.yml @@ -22,7 +22,7 @@ en: too_long: is too long choices: missing: are not complete - too_many: are too many + too_many: You can choose a maximum of %{count}. questionnaire: request_invalid: There was an error handling the request. Please try again. decidim: diff --git a/decidim-meetings/app/cells/decidim/meetings/question_responses/show.erb b/decidim-meetings/app/cells/decidim/meetings/question_responses/show.erb index 1df4b465a74b3..b608da1969303 100644 --- a/decidim-meetings/app/cells/decidim/meetings/question_responses/show.erb +++ b/decidim-meetings/app/cells/decidim/meetings/question_responses/show.erb @@ -1,7 +1,11 @@ <% answer_options_with_percentages.each do |(answer_option_body, answer_percentage)| %> -
    -
    <%= answer_percentage %>
    -
    +
    +
    +
    +
    + <%= translated_attribute(answer_option_body) %> + <%= answer_percentage %> +
    <% end %> diff --git a/decidim-meetings/app/commands/decidim/meetings/admin/update_questionnaire.rb b/decidim-meetings/app/commands/decidim/meetings/admin/update_questionnaire.rb index 34ef576514947..1c5c68098d30f 100644 --- a/decidim-meetings/app/commands/decidim/meetings/admin/update_questionnaire.rb +++ b/decidim-meetings/app/commands/decidim/meetings/admin/update_questionnaire.rb @@ -25,10 +25,7 @@ def call Decidim::Meetings::Questionnaire.transaction do create_questionnaire_for create_questionnaire - if @questionnaire.questions_editable? - update_questionnaire_questions - delete_answers - end + update_questionnaire_questions @questionnaire end end @@ -48,10 +45,25 @@ def create_questionnaire def update_questionnaire_questions @form.questions.each do |form_question| - update_questionnaire_question(form_question) + if form_question.editable? + update_questionnaire_question(form_question) + else + update_questionnaire_question_position(form_question) + end end end + def update_questionnaire_question_position(form_question) + record = @questionnaire.questions.find_by(id: form_question.id) + return if record.blank? + + position = form_question.position + + return if position == record.position + + record.update!(position:) + end + def update_questionnaire_question(form_question) question_attributes = { body: form_question.body, @@ -86,10 +98,6 @@ def update_nested_model(form, attributes, parent_association) record.save! end end - - def delete_answers - @questionnaire.answers.destroy_all - end end end end diff --git a/decidim-meetings/app/commands/decidim/meetings/create_answer.rb b/decidim-meetings/app/commands/decidim/meetings/create_answer.rb index 1ce46716fc460..a654e87676b5f 100644 --- a/decidim-meetings/app/commands/decidim/meetings/create_answer.rb +++ b/decidim-meetings/app/commands/decidim/meetings/create_answer.rb @@ -41,7 +41,7 @@ def answer_question form.selected_choices.each do |choice| answer.choices.build( - body: choice.body, + body: choice.body || translated_attribute(AnswerOption.find_by(id: choice.answer_option_id)&.body), decidim_answer_option_id: choice.answer_option_id ) end diff --git a/decidim-meetings/app/controllers/decidim/meetings/admin/meetings_poll_controller.rb b/decidim-meetings/app/controllers/decidim/meetings/admin/meetings_poll_controller.rb index 0df613b980715..52f082f3733a9 100644 --- a/decidim-meetings/app/controllers/decidim/meetings/admin/meetings_poll_controller.rb +++ b/decidim-meetings/app/controllers/decidim/meetings/admin/meetings_poll_controller.rb @@ -23,8 +23,20 @@ def edit def update enforce_permission_to(:update, :poll, meeting:, poll:) + current_questions_forms = form(Admin::QuestionnaireForm).from_model(questionnaire).questions @form = form(Admin::QuestionnaireForm).from_params(params) + # Although the question values (except the position) will be ignored if they are not editable, + # its information is completed so that if any update failure occurs, the form is rendered again + # with the full data for the disabled questions. + @form.questions = @form.questions.map do |question_form| + next question_form if question_form.editable? + + full_question_form = current_questions_forms.find { |form| form.id.to_s == question_form.id.to_s } + full_question_form.position = question_form.position + full_question_form + end + Admin::UpdateQuestionnaire.call(@form, questionnaire) do on(:ok) do # i18n-tasks-use t("decidim.forms.admin.questionnaires.update.success") diff --git a/decidim-meetings/app/controllers/decidim/meetings/polls/answers_controller.rb b/decidim-meetings/app/controllers/decidim/meetings/polls/answers_controller.rb index cc55a55478340..c4abd850e8b9f 100644 --- a/decidim-meetings/app/controllers/decidim/meetings/polls/answers_controller.rb +++ b/decidim-meetings/app/controllers/decidim/meetings/polls/answers_controller.rb @@ -9,6 +9,14 @@ class AnswersController < Decidim::Meetings::ApplicationController helper_method :question + def admin + enforce_permission_to(:update, :poll, meeting:) + end + + def index + enforce_permission_to(:reply_poll, :meeting, meeting:) + end + def create enforce_permission_to(:create, :answer, question:) @form = form(AnswerForm).from_params(params.merge(question:, current_user:)) diff --git a/decidim-meetings/app/forms/decidim/meetings/admin/question_form.rb b/decidim-meetings/app/forms/decidim/meetings/admin/question_form.rb index fd91c47c02692..f5f24bdfa8820 100644 --- a/decidim-meetings/app/forms/decidim/meetings/admin/question_form.rb +++ b/decidim-meetings/app/forms/decidim/meetings/admin/question_form.rb @@ -18,10 +18,10 @@ class QuestionForm < Decidim::Form translatable_attribute :description, String validates :position, numericality: { greater_than_or_equal_to: 0 } - validates :question_type, inclusion: { in: Decidim::Meetings::Question::QUESTION_TYPES } - validates :max_choices, numericality: { only_integer: true, greater_than: 1, less_than_or_equal_to: ->(form) { form.number_of_options } }, allow_blank: true + validates :question_type, inclusion: { in: Decidim::Meetings::Question::QUESTION_TYPES }, if: :editable? + validates :max_choices, numericality: { only_integer: true, greater_than: 1, less_than_or_equal_to: ->(form) { form.number_of_options } }, allow_blank: true, if: :editable? validates :body, translatable_presence: true, if: :requires_body? - validates :answer_options, presence: true + validates :answer_options, presence: true, if: :editable? def to_param return id if id.present? @@ -29,6 +29,10 @@ def to_param "questionnaire-question-id" end + def editable? + @editable ||= id.blank? || Decidim::Meetings::Question.unpublished.unanswered.exists?(id:) + end + def number_of_options answer_options.size end @@ -36,7 +40,7 @@ def number_of_options private def requires_body? - !deleted + editable? && !deleted end end end diff --git a/decidim-meetings/app/forms/decidim/meetings/answer_form.rb b/decidim-meetings/app/forms/decidim/meetings/answer_form.rb index 4a2056e328ecf..81ca12355f71f 100644 --- a/decidim-meetings/app/forms/decidim/meetings/answer_form.rb +++ b/decidim-meetings/app/forms/decidim/meetings/answer_form.rb @@ -24,8 +24,8 @@ def answer @answer ||= Decidim::Meetings::Answer.find_by(decidim_user_id: current_user.id, decidim_question_id: question_id) if current_user end - def label(idx) - base = "#{idx + 1}. #{translated_attribute(question.body)}" + def label + base = translated_attribute(question.body) base += " (#{max_choices_label})" if question.max_choices base end @@ -43,13 +43,13 @@ def map_model(model) end def selected_choices - choices.select(&:body) + choices.select(&:answer_option_id) end private def max_choices - errors.add(:choices, :too_many) if selected_choices.size > question.max_choices + errors.add(:choices, :too_many, count: question.max_choices) if selected_choices.size > question.max_choices end def mandatory_label diff --git a/decidim-meetings/app/models/decidim/meetings/poll.rb b/decidim-meetings/app/models/decidim/meetings/poll.rb index 9f68f180c8f21..57c620f41e731 100644 --- a/decidim-meetings/app/models/decidim/meetings/poll.rb +++ b/decidim-meetings/app/models/decidim/meetings/poll.rb @@ -14,6 +14,14 @@ class Poll < Meetings::ApplicationRecord delegate :organization, to: :meeting QUESTION_TYPES = %w(single_option multiple_option).freeze + + def has_questions? + questionnaire&.questions&.exists? + end + + def has_open_questions? + has_questions? && questionnaire.questions.not_closed.exists? + end end end end diff --git a/decidim-meetings/app/models/decidim/meetings/question.rb b/decidim-meetings/app/models/decidim/meetings/question.rb index acfb7a379c14e..543ef9bef60a7 100644 --- a/decidim-meetings/app/models/decidim/meetings/question.rb +++ b/decidim-meetings/app/models/decidim/meetings/question.rb @@ -28,6 +28,7 @@ class Question < Meetings::ApplicationRecord validates :question_type, inclusion: { in: QUESTION_TYPES } scope :visible, -> { where(status: [:published, :closed]) } + scope :unanswered, -> { where.missing(:answers) } def multiple_choice? %w(single_option multiple_option).include?(question_type) diff --git a/decidim-meetings/app/models/decidim/meetings/questionnaire.rb b/decidim-meetings/app/models/decidim/meetings/questionnaire.rb index ad498500e85bc..9d6a91b78ff01 100644 --- a/decidim-meetings/app/models/decidim/meetings/questionnaire.rb +++ b/decidim-meetings/app/models/decidim/meetings/questionnaire.rb @@ -11,12 +11,6 @@ class Questionnaire < Meetings::ApplicationRecord has_many :questions, -> { order(:position) }, class_name: "Question", foreign_key: "decidim_questionnaire_id", dependent: :destroy has_many :answers, class_name: "Answer", foreign_key: "decidim_questionnaire_id", dependent: :destroy - # Public: returns whether the questionnaire questions can be modified or not. - def questions_editable? - has_component = questionnaire_for.meeting.respond_to? :component - (has_component && !questionnaire_for.meeting.component.published?) || answers.empty? - end - def all_questions_unpublished? questions.all?(&:unpublished?) end diff --git a/decidim-meetings/app/packs/src/decidim/meetings/meetings_polls.js b/decidim-meetings/app/packs/src/decidim/meetings/meetings_polls.js index 167c561f00ed5..eb9988a52e3b1 100644 --- a/decidim-meetings/app/packs/src/decidim/meetings/meetings_polls.js +++ b/decidim-meetings/app/packs/src/decidim/meetings/meetings_polls.js @@ -9,6 +9,8 @@ $(() => { if ($container.length) { const poll = new MeetingsPollComponent($container, $container.data("decidim-meetings-poll"), $counter); + poll.mountComponent(); + $(".meeting-polls__action-list").on("click", (event) => { event.preventDefault(); @@ -29,6 +31,9 @@ $(() => { if ($adminContainer.length) { const adminPoll = new MeetingsPollComponent($adminContainer, $adminContainer.data("decidim-admin-meetings-poll")); + + adminPoll.mountComponent(); + $(".meeting-polls__action-administrate").on("click", (event) => { event.preventDefault(); diff --git a/decidim-meetings/app/packs/src/decidim/meetings/poll.component.js b/decidim-meetings/app/packs/src/decidim/meetings/poll.component.js index f38ae905bec90..71780d4bb5a41 100644 --- a/decidim-meetings/app/packs/src/decidim/meetings/poll.component.js +++ b/decidim-meetings/app/packs/src/decidim/meetings/poll.component.js @@ -27,6 +27,8 @@ export default class PollComponent { this.pollingInterval = config.pollingInterval || 5000; this.mounted = false; this.questions = {}; + this.questionsStatuses = {}; + this.answersStatuses = {}; } /** @@ -92,6 +94,12 @@ export default class PollComponent { $("[data-question]", $parent).each((_i, el) => { const $el = $(el); const questionId = $el.data("question"); + const elForm = $el.find("form"); + + this.questionsStatuses[questionId] = $el.data("status"); + if (elForm.length > 0) { + this.answersStatuses[questionId] = Object.fromEntries(new FormData(elForm[0])); + } if ($el[0].open === true) { this.questions[questionId] = OPEN; } else { @@ -124,12 +132,28 @@ export default class PollComponent { const questionId = $el.data("question"); // Current question state const state = this.questions[questionId]; + const questionStatus = this.questionsStatuses[questionId]; + const answersStatuses = this.answersStatuses[questionId]; + // New questions have a special class if (!state) { $el.addClass("is-new"); } else if (state === OPEN) { $el.prop(OPEN, true); } + + if ($el.data("status") === CLOSED && $el.data("status") !== questionStatus) { + $el.data("status", `${CLOSED}-new`); + document.getElementById(`closed-announcement-${questionId}`).hidden = false; + } + + if (answersStatuses) { + for (const [key, value] of Object.entries(answersStatuses)) { + if (key.includes("[choices]")) { + $el.find(`[name='${key}'][value='${value}']`).prop("checked", true); + } + } + } } /** @@ -177,7 +201,7 @@ export default class PollComponent { }); }); - $.unique($(".js-check-box-collection").parents(".answer")).each((idx, el) => { + $.unique($(".js-check-box-collection").parents("[data-max-choices]")).each((idx, el) => { const maxChoices = $(el).data("max-choices"); if (maxChoices) { createMaxChoicesAlertComponent({ diff --git a/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss b/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss index 99833b3ffbe35..d975028f66976 100644 --- a/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss +++ b/decidim-meetings/app/packs/stylesheets/decidim/meetings/_item.scss @@ -175,3 +175,107 @@ } } } + +// layout reset stuff +.meeting-poll__layout { + header, + main h1, + footer { + @apply hidden md:block; + } + + .layout-1col { + padding: 0; + } +} + +.meeting-polls { + @apply m-0 md:mt-10 md:mb-24; + + counter-reset: question; + + &__question { + @apply bg-white; + + summary { + @apply p-4 font-normal text-black text-md transition bg-background cursor-pointer marker:text-secondary; + + transition: background-color 0.2s ease-in-out; + + & > span:first-child::after { + counter-increment: question; + content: "#" counter(question); + } + + & > span:last-child:not(:only-child) { + @apply text-sm font-semibold float-right; + } + + & + * { + @apply mt-4 mb-8 md:mb-16 pr-4 pl-[calc(1rem+14px)] md:px-0 space-y-6; + } + } + + &[open] summary { + @apply bg-secondary md:bg-background marker:text-white md:marker:text-secondary text-white md:text-black; + } + + &.is-new { + animation: animateHighlight 5s ease-in-out forwards; + } + + & + & { + @apply mt-4; + } + + @keyframes animateHighlight { + 0%, + 80% { + background-color: rgba(var(--warning-rgb), 0.1); + } + } + } + + &__answer { + label { + @apply block p-4 ring-4 ring-background rounded transition; + + &:not(:has([disabled])) { + @apply hover:ring-tertiary hover:cursor-pointer; + } + } + + label + label, + & + & { + @apply mt-4; + } + + &--value { + @apply flex gap-2 justify-between text-gray-2 text-lg; + + > *:last-child { + @apply w-1/6 flex-none text-gray text-right; + } + } + + &--bar { + @apply w-full h-2.5 overflow-hidden rounded bg-background; + + > * { + @apply bg-success h-full rounded; + } + } + } + + &__admin-action { + @apply py-4 grid grid-cols-2 gap-x-4 gap-y-8 border-t border-t-gray [&>*:nth-child(2)]:ml-auto [&>*:nth-child(3)]:col-span-2; + } + + &__topbar { + @apply my-4 md:my-0 px-4 md:px-0 py-2 md:py-10 flex justify-between gap-4 bg-background md:bg-transparent; + + &.is-admin { + @apply mt-0 bg-tertiary md:bg-transparent text-black; + } + } +} diff --git a/decidim-meetings/app/packs/stylesheets/decidim/meetings/_live_event.scss b/decidim-meetings/app/packs/stylesheets/decidim/meetings/_live_event.scss index db2628cb1c30c..f27c8fb201286 100644 --- a/decidim-meetings/app/packs/stylesheets/decidim/meetings/_live_event.scss +++ b/decidim-meetings/app/packs/stylesheets/decidim/meetings/_live_event.scss @@ -18,8 +18,6 @@ &__aside { @apply flex-none bg-background-2; - counter-reset: question; - &.is-open { @apply w-1/5 [&+div]:w-4/5; @@ -33,96 +31,4 @@ } } } - - &__question { - @apply bg-white; - - summary { - @apply p-4 cursor-pointer font-bold text-secondary text-md transition border-b border-b-background hover:bg-background; - - transition: background-color 0.2s ease-in-out; - - &::after { - counter-increment: question; - content: "#" counter(question); - } - - & ~ * { - @apply p-4; - - // dynamic content - > :first-child { - @apply font-bold; - } - - label { - @apply flex items-baseline cursor-pointer; - } - - label + label { - @apply mt-4; - } - // end dynamic content - } - } - - &[open] .meeting-polls__answer--bar > * { - opacity: 1; - transform: translateX(0); - } - - &.is-new { - animation: animateHighlight 5s ease-in-out forwards; - } - - @keyframes animateHighlight { - 0%, - 80% { - background-color: rgba(var(--warning-rgb), 0.1); - } - } - } - - &__question--admin { - summary ~ * { - @apply border-t border-t-background p-4 bg-background-2; - - a { - @apply my-4 block text-sm underline; - } - - > :first-child { - @apply font-normal; - } - } - } - - &__answer { - @apply flex items-center gap-2; - - &--value { - @apply w-1/5 font-bold; - } - - &--bar { - @apply w-4/5 h-2.5 overflow-hidden; - - > * { - @apply bg-primary h-full opacity-0; - } - } - } - - // vertical flow - label + &__answer + label { - @apply mt-6; - } - - &__admin-label { - @apply p-4 bg-secondary text-white font-bold text-sm; - } - - &__admin-action { - @apply py-2 flex items-center border-t-background [&>*]:flex-none first:[&>*]:w-2/5 last:[&>*]:w-3/5; - } } diff --git a/decidim-meetings/app/permissions/decidim/meetings/permissions.rb b/decidim-meetings/app/permissions/decidim/meetings/permissions.rb index 4b5fff02751de..14c8ac57c77e4 100644 --- a/decidim-meetings/app/permissions/decidim/meetings/permissions.rb +++ b/decidim-meetings/app/permissions/decidim/meetings/permissions.rb @@ -39,6 +39,13 @@ def permissions toggle_allow(can_close_meeting?) when :register toggle_allow(can_register_invitation_meeting?) + when :reply_poll + toggle_allow(can_reply_poll?) + end + when :poll + case permission_action.action + when :update + toggle_allow(can_update_poll?) end else return permission_action @@ -112,6 +119,19 @@ def can_register_invitation_meeting? authorized?(:register, resource: meeting) end + def can_reply_poll? + meeting.present? && + meeting.poll.present? && + authorized?(:reply_poll, resource: meeting) + end + + def can_update_poll? + user.present? && + user.admin? && + meeting.present? && + meeting.poll.present? + end + def can_answer_question? question.present? && user.present? && !question.answered_by?(user) end diff --git a/decidim-meetings/app/views/decidim/meetings/admin/poll/_answer_option_template.html.erb b/decidim-meetings/app/views/decidim/meetings/admin/poll/_answer_option_template.html.erb index 66b765e453e5f..061d6f4e9b227 100644 --- a/decidim-meetings/app/views/decidim/meetings/admin/poll/_answer_option_template.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/admin/poll/_answer_option_template.html.erb @@ -2,6 +2,6 @@ diff --git a/decidim-meetings/app/views/decidim/meetings/admin/poll/_form.html.erb b/decidim-meetings/app/views/decidim/meetings/admin/poll/_form.html.erb index a9b9bed194f23..c74cc0016772f 100644 --- a/decidim-meetings/app/views/decidim/meetings/admin/poll/_form.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/admin/poll/_form.html.erb @@ -4,19 +4,24 @@ - <% if questionnaire.questions_editable? %> - <%= fields_for "questionnaire[questions][#{blank_question.to_param}]", blank_question do |question_form| %> - - <%= render "decidim/meetings/admin/poll/answer_option_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "answer-option-template-dummy" %> - <% end %> - <% else %> - <%= cell("decidim/announcement", t("already_answered_warning", scope: "decidim.forms.admin.questionnaires.form"), callout_class: "warning" ) %> + <% announcement_body = capture do %> +
      + <% t("announcement_html", admin_link: Decidim::EngineRouter.main_proxy(meeting.component).admin_meeting_polls_answers_path(meeting), scope: "decidim.meetings.admin.poll.form").each do |item| %> +
    • <%= item %>
    • + <% end %> +
    + <% end %> + <%= cell("decidim/announcement", { body: announcement_body }, callout_class: "info") %> + + <%= fields_for "questionnaire[questions][#{blank_question.to_param}]", blank_question do |question_form| %> + + <%= render "decidim/meetings/admin/poll/answer_option_template", form: question_form, editable: true, template_id: "answer-option-template-dummy" %> <% end %>
    @@ -25,24 +30,20 @@ <%= render "decidim/meetings/admin/poll/question", form: question_form, id: tabs_id_for_question(question), - editable: questionnaire.questions_editable?, + editable: question.editable?, answer_option_template_selector: "#answer-option-template-#{index}" %> - <%= render "decidim/meetings/admin/poll/answer_option_template", form: question_form, editable: questionnaire.questions_editable?, template_id: "answer-option-template-#{index}" %> + <%= render "decidim/meetings/admin/poll/answer_option_template", form: question_form, editable: question.editable?, template_id: "answer-option-template-#{index}" %> <% end %> <% end %>
    - <% if questionnaire.questions_editable? %> - - <% end %> + <%= append_javascript_pack_tag "decidim_forms_admin" %> -<% if questionnaire.questions_editable? %> - -<% end %> + diff --git a/decidim-meetings/app/views/decidim/meetings/admin/poll/_question.html.erb b/decidim-meetings/app/views/decidim/meetings/admin/poll/_question.html.erb index b2f1a681499e3..6f09f7decb53d 100644 --- a/decidim-meetings/app/views/decidim/meetings/admin/poll/_question.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/admin/poll/_question.html.erb @@ -7,6 +7,8 @@ <% if editable %> <%== icon("drag-move-2-fill") %> + <% else %> + <%== icon("lock-line") %> <% end %> <%= dynamic_title(translated_attribute(question.body), class: "question-title-statement", max_length: 50, omission: "...", placeholder: t("question", scope: "decidim.forms.admin.questionnaires.question")) %> @@ -22,17 +24,17 @@ - <% if editable %> - + - + + <% if editable %> @@ -66,17 +68,17 @@ <% if question.persisted? %> - <%= form.hidden_field :id, disabled: !editable %> + <%= form.hidden_field :id %> <% end %> - <%= form.hidden_field :position, value: question.position || 0, disabled: !editable %> + <%= form.hidden_field :position, value: question.position || 0 %> <%= form.hidden_field :deleted, disabled: !editable %> -
    +
    " data-template="<%= answer_option_template_selector %>">
    <% question.answer_options.each do |answer_option| %> <%= fields_for "questionnaire[questions][#{question.to_param}][answer_options][]", answer_option do |answer_option_form| %> - <%= render "decidim/meetings/admin/poll/answer_option", form: answer_option_form, question:, editable: %> + <%= render "decidim/meetings/admin/poll/answer_option", form: answer_option_form, question:, editable: question.editable? %> <% end %> <% end %>
    @@ -87,7 +89,7 @@ <% end %>
    -
    +
    "> <%= form.select( :max_choices, diff --git a/decidim-meetings/app/views/decidim/meetings/admin/poll/edit.html.erb b/decidim-meetings/app/views/decidim/meetings/admin/poll/edit.html.erb index 586fa04500650..46fac68bff57e 100644 --- a/decidim-meetings/app/views/decidim/meetings/admin/poll/edit.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/admin/poll/edit.html.erb @@ -17,13 +17,11 @@ <%= decidim_form_for(@form, url: update_url, method: :put, html: { class: "form-defaults form edit_questionnaire" }) do |form| %> <%= render partial: "decidim/meetings/admin/poll/form", object: form %> - <% if questionnaire.questions_editable? %> -
    -
    - <%= form.submit t("save", scope: "decidim.forms.admin.questionnaires.edit"), class: "button button__sm button__secondary" %> -
    +
    +
    + <%= form.submit t("save", scope: "decidim.forms.admin.questionnaires.edit"), class: "button button__sm button__secondary" %>
    - <% end %> +
    <% end %>
    diff --git a/decidim-meetings/app/views/decidim/meetings/layouts/live_event.html.erb b/decidim-meetings/app/views/decidim/meetings/layouts/live_event.html.erb index 3fdfdd81ef623..bb8ee92d06493 100644 --- a/decidim-meetings/app/views/decidim/meetings/layouts/live_event.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/layouts/live_event.html.erb @@ -14,18 +14,6 @@ <%= render partial: "layouts/decidim/timeout_modal" %>
    - <% if current_user && poll %> -
    - - - <% if admin_allowed_to?(:update, :poll, meeting: meeting, poll: poll) %> - - <% end %> -
    - <% end %> -
    <%= current_organization_name %> / <%= present(meeting).title(links: true, html_escape: true ) %>
    @@ -43,8 +31,6 @@
    - - <%= yield %>
    diff --git a/decidim-meetings/app/views/decidim/meetings/meetings/_meeting.html.erb b/decidim-meetings/app/views/decidim/meetings/meetings/_meeting.html.erb index e79589613f174..1c05ea9b53879 100644 --- a/decidim-meetings/app/views/decidim/meetings/meetings/_meeting.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/meetings/_meeting.html.erb @@ -3,6 +3,7 @@

    <%= present(meeting).title(links: true, html_escape: true ) %>

    <%= cell "decidim/meetings/dates_and_map", meeting %> + <%= render partial: "meeting_poll_actions", locals: { mobile: true } %>
    <%= cell "decidim/author", author_presenter_for(meeting.normalized_author), from: meeting, context_actions: nil, layout: :compact %> diff --git a/decidim-meetings/app/views/decidim/meetings/meetings/_meeting_aside.html.erb b/decidim-meetings/app/views/decidim/meetings/meetings/_meeting_aside.html.erb index 1c7ae2d529ed5..d081974690962 100644 --- a/decidim-meetings/app/views/decidim/meetings/meetings/_meeting_aside.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/meetings/_meeting_aside.html.erb @@ -1,3 +1,5 @@ +<%= render partial: "meeting_poll_actions" %> + <% if meeting.can_be_joined_by?(current_user) || meeting.on_different_platform? %>
    <%= cell "decidim/meetings/join_meeting_button", meeting, show_remaining_slots: true %> diff --git a/decidim-meetings/app/views/decidim/meetings/meetings/_meeting_poll_actions.html.erb b/decidim-meetings/app/views/decidim/meetings/meetings/_meeting_poll_actions.html.erb new file mode 100644 index 0000000000000..90d8bfae5376b --- /dev/null +++ b/decidim-meetings/app/views/decidim/meetings/meetings/_meeting_poll_actions.html.erb @@ -0,0 +1,15 @@ +<% extra_classes = local_assigns.fetch(:mobile, false) ? " block md:hidden" : " hidden md:block" %> +<% if meeting.poll&.has_questions? %> +
    + <%= action_authorized_link_to :reply_poll, meeting_polls_answers_path(meeting), class: "button button__xl button__secondary w-full", data: { "redirect_url" => meeting_polls_answers_path(meeting) }, resource: meeting do %> + + <%= meeting.poll.has_open_questions? ? t("reply_poll", scope: "decidim.meetings.meetings.meeting") : t("view_poll", scope: "decidim.meetings.meetings.meeting") %> + + <% end %> + <% if !allowed_to?(:reply_poll, :meeting, meeting:) && allowed_to?(:update, :poll, meeting:) %> + <%= link_to admin_meeting_polls_answers_path(meeting), class: "button button__sm button__transparent-secondary w-full" do %> + <%= t("administrate", scope: "decidim.meetings.polls.answers.index") %> + <% end %> + <% end %> +
    +<% end %> diff --git a/decidim-meetings/app/views/decidim/meetings/polls/answers/_multiple_option.html.erb b/decidim-meetings/app/views/decidim/meetings/polls/answers/_multiple_option.html.erb index 03d697942f5e6..8de24f50f2abd 100644 --- a/decidim-meetings/app/views/decidim/meetings/polls/answers/_multiple_option.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/polls/answers/_multiple_option.html.erb @@ -2,16 +2,12 @@ <% question.answer_options.each_with_index do |answer_option, idx| %> <% choice = answer.choices.find { |choice| choice.decidim_answer_option_id == answer_option.id } if answer %> -
    - <%= label_tag do %> - <%= check_box_tag "answer[choices][#{idx}][body]", - translated_attribute(answer_option.body), - choice.present?, disabled: %> + <%= label_tag "answer[choices][#{answer_option.id}][answer_option_id]", nil, class: "js-collection-input" do %> + <%= check_box_tag "answer[choices][#{answer_option.id}][answer_option_id]", + answer_option.id, + choice.present?, disabled: %> - <%= translated_attribute(answer_option.body) %> - - <%= hidden_field_tag "answer[choices][#{idx}][answer_option_id]", answer_option.id, disabled: %> - <% end %> -
    + <%= translated_attribute(answer_option.body) %> + <% end %> <% end %>
    diff --git a/decidim-meetings/app/views/decidim/meetings/polls/answers/_single_option.html.erb b/decidim-meetings/app/views/decidim/meetings/polls/answers/_single_option.html.erb index 9a87910774285..b9b1595ccbb92 100644 --- a/decidim-meetings/app/views/decidim/meetings/polls/answers/_single_option.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/polls/answers/_single_option.html.erb @@ -3,17 +3,11 @@ <% question.answer_options.each_with_index do |answer_option, idx| %> <% choice_id = "#{field_id}_choices_#{idx}" %> - <%= label_tag "#{choice_id}_body" do %> - <%= radio_button_tag "answer[choices][#{question.id}][body]", - translated_attribute(answer_option.body), + <%= label_tag "#{choice_id}_answer_option" do %> + <%= radio_button_tag "answer[choices][#{question.id}][answer_option_id]", + answer_option.id, answer_option.id == choice.try(:decidim_answer_option_id), - id: "#{choice_id}_body", disabled: %> - + id: "#{choice_id}_answer_option", disabled: %> <%= translated_attribute(answer_option.body) %> - - <%= hidden_field_tag "answer[choices][#{question.id}][answer_option_id]", - answer_option.id, - id: "#{choice_id}_answer_option", - disabled: %> <% end %> <% end %> diff --git a/decidim-meetings/app/views/decidim/meetings/polls/answers/admin.html.erb b/decidim-meetings/app/views/decidim/meetings/polls/answers/admin.html.erb new file mode 100644 index 0000000000000..e72b89ad5b57d --- /dev/null +++ b/decidim-meetings/app/views/decidim/meetings/polls/answers/admin.html.erb @@ -0,0 +1,34 @@ +<% add_decidim_meta_tags({ + title: present(meeting).title, + description: present(meeting).description, + url: meeting_url(meeting.id) + }) %> + +<%= append_javascript_pack_tag "decidim_meetings" %> +<%= append_stylesheet_pack_tag "decidim_meetings" %> +<%= append_javascript_pack_tag "decidim_forms" %> + +<% add_body_classes "meeting-poll__layout" %> + +<%= render layout: "layouts/decidim/shared/layout_center" do %> +
    + <%= link_to meeting_path(meeting), class: "button button__sm button__text md:button__text-secondary" do %> + <%= icon "arrow-left-line" %> + <%= t("back_to_meeting", scope: "decidim.meetings.polls.answers.index_admin") %> + <% end %> + + <%= action_authorized_link_to :reply_poll, meeting_polls_answers_path(meeting), class: "button button__sm button__secondary", data: { "redirect_url" => meeting_polls_answers_path(meeting) }, resource: meeting do %> + <%= t("view_poll", scope: "decidim.meetings.polls.answers.index_admin") %> + <% end %> +
    + +
    +

    + <%= t("title", scope: "decidim.meetings.polls.answers.index_admin") %> +

    +
    + +
    + <%= render partial: "decidim/meetings/polls/questions/index_admin", locals: { open_question: nil } %> +
    +<% end %> diff --git a/decidim-meetings/app/views/decidim/meetings/polls/answers/index.html.erb b/decidim-meetings/app/views/decidim/meetings/polls/answers/index.html.erb new file mode 100644 index 0000000000000..08c57d2284a14 --- /dev/null +++ b/decidim-meetings/app/views/decidim/meetings/polls/answers/index.html.erb @@ -0,0 +1,36 @@ +<% add_decidim_meta_tags({ + title: present(meeting).title, + description: present(meeting).description, + url: meeting_url(meeting.id) + }) %> + +<%= append_javascript_pack_tag "decidim_meetings" %> +<%= append_stylesheet_pack_tag "decidim_meetings" %> +<%= append_javascript_pack_tag "decidim_forms" %> + +<% add_body_classes "meeting-poll__layout" %> + +<%= render layout: "layouts/decidim/shared/layout_center" do %> +
    + <%= link_to meeting_path(meeting), class: "button button__sm button__text-secondary" do %> + <%= icon "arrow-left-line" %> + <%= t("back_to_meeting", scope: "decidim.meetings.polls.answers.index_admin") %> + <% end %> + + <% if admin_allowed_to?(:update, :poll, meeting: meeting, poll: poll) %> + <%= link_to admin_meeting_polls_answers_path(meeting), class: "button button__sm button__primary" do %> + <%= t("administrate", scope: "decidim.meetings.polls.answers.index") %> + <% end %> + <% end %> +
    + +
    +

    + <%= t("title", scope: "decidim.meetings.polls.answers.index") %> +

    +
    + +
    + <%= render partial: "decidim/meetings/polls/questions/index" %> +
    +<% end %> diff --git a/decidim-meetings/app/views/decidim/meetings/polls/questions/_closed_question.html.erb b/decidim-meetings/app/views/decidim/meetings/polls/questions/_closed_question.html.erb index 9d17a170e5951..c6d54634b0df4 100644 --- a/decidim-meetings/app/views/decidim/meetings/polls/questions/_closed_question.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/polls/questions/_closed_question.html.erb @@ -1,7 +1,13 @@ -<%= t(".question_results") %> + + <%= t(".question") %> + <%= t(".question_results") %> +
    -

    <%= translated_attribute(question.body) %>

    +

    <%= translated_attribute(question.body) %>

    + " hidden> + <%= cell "decidim/announcement", t("announcement", scope: "decidim.meetings.polls.questions.closed_question"), callout_class: "warning" %> + <%= cell "decidim/meetings/question_responses", question %>
    diff --git a/decidim-meetings/app/views/decidim/meetings/polls/questions/_index_admin.html.erb b/decidim-meetings/app/views/decidim/meetings/polls/questions/_index_admin.html.erb index 4483b8becbeb4..9251299bc78a6 100644 --- a/decidim-meetings/app/views/decidim/meetings/polls/questions/_index_admin.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/polls/questions/_index_admin.html.erb @@ -1,40 +1,43 @@ -
    <%= t(".admin_dashboard") %>
    - <% questionnaire.questions.includes([:questionnaire]).each do |question| %>
    > - <%= t(".question") %> + + <%= t(".question") %> + <%= t(question.status, scope: "decidim.meetings.polls.questions.index_admin.statuses") %> + <%= form_tag(meeting_polls_question_path(meeting, question), method: :patch, remote: true) do %> -
    -

    <%= translated_attribute(question.body) %>

    +

    <%= translated_attribute(question.body) %>

    - <%= link_to t(".edit"), Decidim::EngineRouter.admin_proxy(meeting.component).edit_meeting_poll_path(meeting), target: "_blank", rel: "noopener noreferrer" %> + <% if question.unpublished? %> + <%= link_to( + t(".edit"), + Decidim::EngineRouter.admin_proxy(meeting.component).edit_meeting_poll_path(meeting), + class: "button button__xs button__secondary mr-auto mt-2", + target: "_blank", + rel: "noopener noreferrer" + ) %> + <% end %> +
    -
    <%= t(".question") %>
    -
    - <% if question.unpublished? %> - - <% else %> -
    <%= t(".sent") %>
    -
    <%= pluralize(question.answers_count, t(".received_answer"), t(".received_answers")) %>
    - <% end %> -
    +
    <%= t(".question") %><%= " · " + t(".sent") if question.published? %>
    + + <% if question.unpublished? %> + + <% else %> +
    <%= pluralize(question.answers_count, t(".received_answer"), t(".received_answers")) %>
    + <% end %>
    +
    -
    <%= t(".results") %>
    +
    <%= t(".results") %>
    + + +
    <% unless question.unpublished? %> <%= cell "decidim/meetings/question_responses", question %> <% end %> - - <% if question.unpublished? %> - - <% elsif question.published? %> - - <% elsif question.closed? %> -
    <%= t(".sent") %>
    - <% end %>
    diff --git a/decidim-meetings/app/views/decidim/meetings/polls/questions/_published_question.html.erb b/decidim-meetings/app/views/decidim/meetings/polls/questions/_published_question.html.erb index 288b6a11c120d..ac1e46df11343 100644 --- a/decidim-meetings/app/views/decidim/meetings/polls/questions/_published_question.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/polls/questions/_published_question.html.erb @@ -1,11 +1,15 @@ -<%= t(".question") %> + + <%= t(".question") %> +
    -

    <%= translated_attribute(question.body) %>

    <% @form = form || Decidim::Meetings::AnswerForm.new(question_id: question.id, current_user:) %> - <%= decidim_form_for(@form, url: meeting_polls_answers_path(meeting), method: :post, remote: true, html: { class: "form-defaults mt-4" }, data: { "safe-path" => meeting_live_event_path(meeting) }) do |form| %> -
    + +

    <%= @form.label %>

    + + <%= decidim_form_for(@form, url: meeting_polls_answers_path(meeting), method: :post, remote: true, html: { class: "form-defaults" }, data: { "safe-path" => meeting_live_event_path(meeting) }) do |form| %> +

    <%= t(".max_choices_alert") %>

    <%= render partial: "decidim/meetings/polls/answers/#{question.question_type}", locals: { answer: @form.answer, question:, answer_form: form, disabled: question.answered_by?(current_user), field_id: question.id } %> @@ -15,13 +19,12 @@ <% @form.errors.full_messages.each do |msg| %> <%= msg %> <% end %> + + <% if question.answered_by?(current_user) %> + <%= cell("decidim/announcement", t(".question_replied"), callout_class: "success" ) %> + <% else %> + + <% end %>
    - <% if question.answered_by?(current_user) %> - <%= cell("decidim/announcement", t(".question_replied"), callout_class: "success" ) %> - <% else %> -
    - -
    - <% end %> <% end %>
    diff --git a/decidim-meetings/app/views/decidim/meetings/polls/questions/_question.html.erb b/decidim-meetings/app/views/decidim/meetings/polls/questions/_question.html.erb index 0ae73574f3d21..82b940203d318 100644 --- a/decidim-meetings/app/views/decidim/meetings/polls/questions/_question.html.erb +++ b/decidim-meetings/app/views/decidim/meetings/polls/questions/_question.html.erb @@ -1,4 +1,4 @@ -
    > +
    > <% if question.published? %> <%= render partial: "decidim/meetings/polls/questions/published_question", locals: { question:, form: local_assigns.fetch(:form, nil) } %> <% elsif question.closed? %> diff --git a/decidim-meetings/app/views/decidim/meetings/polls/questions/index.js.erb b/decidim-meetings/app/views/decidim/meetings/polls/questions/index.js.erb index c0e5e2e65b34e..77d86677794b5 100644 --- a/decidim-meetings/app/views/decidim/meetings/polls/questions/index.js.erb +++ b/decidim-meetings/app/views/decidim/meetings/polls/questions/index.js.erb @@ -1,5 +1,5 @@ -var $aside = $("#meeting-poll-aside"); +var $meetingPoll = $("[data-decidim-meetings-poll]"); -if($aside.length){ - $aside.html('<%= j(render partial: "decidim/meetings/polls/questions/index").strip.html_safe %>'); +if($meetingPoll.length){ + $meetingPoll.html('<%= j(render partial: "decidim/meetings/polls/questions/index").strip.html_safe %>'); } diff --git a/decidim-meetings/app/views/decidim/meetings/polls/questions/index_admin.js.erb b/decidim-meetings/app/views/decidim/meetings/polls/questions/index_admin.js.erb index 701aec97f83a7..f90335453dc52 100644 --- a/decidim-meetings/app/views/decidim/meetings/polls/questions/index_admin.js.erb +++ b/decidim-meetings/app/views/decidim/meetings/polls/questions/index_admin.js.erb @@ -1,5 +1,5 @@ -var $aside = $("#admin-meeting-poll-aside"); +var $adminMeetingPoll = $("[data-decidim-admin-meetings-poll]"); -if($aside.length){ - $aside.html('<%= j(render partial: "decidim/meetings/polls/questions/index_admin", locals: { open_question: }).strip.html_safe %>'); +if($adminMeetingPoll.length){ + $adminMeetingPoll.html('<%= j(render partial: "decidim/meetings/polls/questions/index_admin", locals: { open_question: }).strip.html_safe %>'); } diff --git a/decidim-meetings/config/locales/en.yml b/decidim-meetings/config/locales/en.yml index f11cbd53b0258..56dfd5bfb1249 100644 --- a/decidim-meetings/config/locales/en.yml +++ b/decidim-meetings/config/locales/en.yml @@ -147,6 +147,7 @@ en: actions: comment: Comment join: Join + reply_poll: Reply poll name: Meetings settings: global: @@ -363,6 +364,13 @@ en: update: invalid: There was a problem updating this meeting poll. success: Meeting poll successfully updated. + poll: + form: + announcement_html: + - When a question receives answers or is published/closed, it can no longer be edited. + - You can add a question at any time. + - The poll will be closed when the results of all created questions have been published. + - Visit the poll administration page to send questions and publish results. registrations: edit: save: Save @@ -441,16 +449,14 @@ en: iframe_embed_type: embed_in_meeting_page: Embed in meeting page none: None - open_in_live_event_page: Open in live event page (with optional polls) + open_in_live_event_page: Open in live event page open_in_new_tab: Open URL in a new tab last_activity: meeting_updated: 'Meeting updated:' new_meeting: 'New meeting:' layouts: live_event: - administrate: Administrate close: close - questions: Questions mailer: invite_join_meeting_mailer: invite: @@ -531,6 +537,8 @@ en: edit_close_meeting: Edit meeting report edit_meeting: Edit meeting join_meeting: Join meeting + reply_poll: Reply poll + view_poll: View poll meetings: no_meetings_warning: No meetings match your search criteria or there is not any meeting scheduled. upcoming_meetings_warning: Currently, there are no scheduled meetings, but here you can find all the past meetings listed. @@ -589,13 +597,22 @@ en: start_time: Start date title: Title polls: + answers: + index: + administrate: Administrate + title: Poll + index_admin: + back_to_meeting: Back to meeting + title: Administrate poll + view_poll: View poll questions: closed_question: - question_results: Question results + announcement: The replies for this question have been closed. + question: Question + question_results: Results index: empty_questions: Throughout this meeting, some questions will be sent and you will be able to answer them. They will be displayed here. index_admin: - admin_dashboard: Administrator dashboard edit: Edit in the admin panel question: Question received_answer: received answer @@ -603,6 +620,10 @@ en: results: Results send: Send sent: Sent + statuses: + closed: Results sent (closed) + published: Sent (open) + unpublished: Pending to be sent published_question: max_choices_alert: There are too many choices selected question: Question diff --git a/decidim-meetings/lib/decidim/meetings/component.rb b/decidim-meetings/lib/decidim/meetings/component.rb index 78c4466b6c1b1..6cf5d6b08fdef 100644 --- a/decidim-meetings/lib/decidim/meetings/component.rb +++ b/decidim-meetings/lib/decidim/meetings/component.rb @@ -19,7 +19,7 @@ resource.template = "decidim/meetings/meetings/linked_meetings" resource.card = "decidim/meetings/meeting" resource.reported_content_cell = "decidim/meetings/reported_content" - resource.actions = %w(join comment) + resource.actions = %w(join comment reply_poll) resource.searchable = true end diff --git a/decidim-meetings/lib/decidim/meetings/engine.rb b/decidim-meetings/lib/decidim/meetings/engine.rb index 47566b594096c..7e91fa02774f6 100644 --- a/decidim-meetings/lib/decidim/meetings/engine.rb +++ b/decidim-meetings/lib/decidim/meetings/engine.rb @@ -32,7 +32,11 @@ class Engine < ::Rails::Engine resource :live_event, only: :show namespace :polls do resources :questions, only: [:index, :update] - resources :answers, only: [:index, :create] + resources :answers, only: [:index, :create] do + collection do + get :admin + end + end end end scope "/meetings" do diff --git a/decidim-meetings/spec/models/decidim/meetings/questionnaire_spec.rb b/decidim-meetings/spec/models/decidim/meetings/questionnaire_spec.rb index 7a6622fc50839..ceb4c7ccba3a1 100644 --- a/decidim-meetings/spec/models/decidim/meetings/questionnaire_spec.rb +++ b/decidim-meetings/spec/models/decidim/meetings/questionnaire_spec.rb @@ -34,13 +34,6 @@ module Meetings expect(questionnaire.questionnaire_for).to eq(questionable) end - describe "#questions_editable?" do - it "returns false when questionnaire has already answers" do - create(:meetings_poll_answer, questionnaire:) - expect(subject.reload).not_to be_questions_editable - end - end - describe "#all_questions_unpublished?" do it "returns true when all questionnaire questions are in unpublished state" do subject.questions << create(:meetings_poll_question, :unpublished) diff --git a/decidim-meetings/spec/system/admin/admin_manages_meetings_polls_spec.rb b/decidim-meetings/spec/system/admin/admin_manages_meetings_polls_spec.rb index 8167f3e0d4099..a27985ba0ef66 100644 --- a/decidim-meetings/spec/system/admin/admin_manages_meetings_polls_spec.rb +++ b/decidim-meetings/spec/system/admin/admin_manages_meetings_polls_spec.rb @@ -8,7 +8,7 @@ let(:current_component) { create(:component, participatory_space: participatory_process, manifest_name: "meetings") } let(:manifest_name) { "meetings" } let!(:meeting) { create(:meeting, scope:, services: [], component: current_component) } - let(:poll) { create(:poll) } + let(:poll) { create(:poll, meeting:) } let(:questionnaire) { create(:meetings_poll_questionnaire, questionnaire_for: poll) } let(:body) do { @@ -31,7 +31,7 @@ include_context "when managing a component as an admin" - context "when the questionnaire is not already answered" do + context "when the questionnaire has unpublished questions" do before do visit questionnaire_edit_path end @@ -239,15 +239,134 @@ end end - context "when the questionnaire is already answered" do - let!(:question) { create(:meetings_poll_question, questionnaire:, body:, question_type: "multiple_option") } - let!(:answer) { create(:meetings_poll_answer, questionnaire:, question:) } + context "when the questionnaire includes published and closed questions" do + let!(:unpublished_question) { create(:meetings_poll_question, :unpublished, questionnaire:, question_type: "single_option", position: 0) } + let!(:published_question) { create(:meetings_poll_question, :published, questionnaire:, question_type: "single_option", position: 1) } + let!(:closed_question) { create(:meetings_poll_question, :closed, questionnaire:, question_type: "single_option", position: 2) } - it "can modify questionnaire questions" do + it "displays all questions with inputs disabled for not unpublished questions" do + visit questionnaire_edit_path + + expand_all_questions + + expect(page).to have_css("input[value='#{translated_attribute(unpublished_question.body)}']:not([disabled])") + expect(page).to have_css("input[value='#{translated_attribute(published_question.body)}'][disabled='disabled']") + expect(page).to have_css("input[value='#{translated_attribute(closed_question.body)}'][disabled='disabled']") + end + + it "can create new questions" do + visit questionnaire_edit_path + + click_on "Add question" + + expand_all_questions + + within ".questionnaire-question:last-of-type" do + fill_in find_nested_form_field_locator("body_en"), with: "New question title" + page.all(".questionnaire-question-answer-option").each_with_index do |question_answer_option, answer_option_idx| + within question_answer_option do + fill_in find_nested_form_field_locator("body_en"), with: "New question answer option #{answer_option_idx + 1}" + end + end + end + click_on "Save" + + expect(page).to have_admin_callout("successfully") + + visit_questionnaire_edit_path_and_expand_all + + expect(page).to have_css("input[value='New question title']") + expect(page).to have_css("input[value='New question answer option 1']") + expect(page).to have_css("input[value='New question answer option 2']") + end + + it "can modify questionnaire open questions" do visit questionnaire_edit_path expect(page).to have_content("Add question") - expect(page).to have_no_content("Remove") + expand_all_questions + within "#questionnaire_question_#{unpublished_question.id}-field" do + expect(page).to have_content("Remove") + expect(page).to have_content("Add answer option") + fill_in find_nested_form_field_locator("body_en"), with: "Changed title" + page.all(".questionnaire-question-answer-option").each_with_index do |question_answer_option, answer_option_idx| + within question_answer_option do + fill_in find_nested_form_field_locator("body_en"), with: "Changed answer option #{answer_option_idx + 1}" + end + end + end + + click_on "Save" + + expect(page).to have_admin_callout("successfully") + + visit_questionnaire_edit_path_and_expand_all + + expect(page).to have_css("input[value='Changed title']") + expect(page).to have_css("input[value='Changed answer option 1']") + expect(page).to have_css("input[value='Changed answer option 2']") + expect(page).to have_css("input[value='Changed answer option 3']") + end + + context "when there are validation errors" do + before do + visit questionnaire_edit_path + click_on "Add question" + click_on "Save" + end + + it "keeps the content of blocked questions" do + expect(page).to have_content("There was a problem updating this meeting poll") + expand_all_questions + + expect(page).to have_css("input[value='#{translated_attribute(unpublished_question.body)}']:not([disabled])") + expect(page).to have_css("input[value='#{translated_attribute(published_question.body)}'][disabled='disabled']") + expect(page).to have_css("input[value='#{translated_attribute(closed_question.body)}'][disabled='disabled']") + end + end + + it "can reorder published or closed questions" do + visit questionnaire_edit_path + within "#questionnaire_question_#{unpublished_question.id}-field" do + expect(page).to have_content("Remove") + expect(page).to have_content("Down") + expect(page).to have_no_content("Up") + end + + within "#questionnaire_question_#{published_question.id}-field" do + expect(page).to have_no_content("Remove") + expect(page).to have_content("Down") + expect(page).to have_content("Up") + end + + within "#questionnaire_question_#{closed_question.id}-field" do + expect(page).to have_no_content("Remove") + expect(page).to have_no_content("Down") + expect(page).to have_content("Up") + end + + within "#questionnaire_question_#{closed_question.id}-field" do + click_on "Up" + click_on "Up" + end + + within "#questionnaire_question_#{unpublished_question.id}-field" do + click_on "Down" + end + + click_on "Save" + + expand_all_questions + + within ".questionnaire-question:last-of-type" do + expect(page).to have_css("#questionnaire_question_#{unpublished_question.id}-button") + end + within ".questionnaire-question:first-of-type" do + expect(page).to have_css("#questionnaire_question_#{closed_question.id}-button") + end + expect(unpublished_question.reload.position).to eq(2) + expect(published_question.reload.position).to eq(1) + expect(closed_question.reload.position).to eq(0) end end diff --git a/decidim-meetings/spec/system/live_meeting_admin_questions_spec.rb b/decidim-meetings/spec/system/meeting_questions_administrate_spec.rb similarity index 79% rename from decidim-meetings/spec/system/live_meeting_admin_questions_spec.rb rename to decidim-meetings/spec/system/meeting_questions_administrate_spec.rb index 7fe64d349a523..71ea62ee60a9b 100644 --- a/decidim-meetings/spec/system/live_meeting_admin_questions_spec.rb +++ b/decidim-meetings/spec/system/meeting_questions_administrate_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -describe "Meeting live event poll administration" do +describe "Meeting poll administration" do include_context "when managing a component" do let(:component_organization_traits) { admin_component_organization_traits } end @@ -24,7 +24,7 @@ let(:manifest_name) { "meetings" } - let(:meeting) { create(:meeting, :published, :online, :live, component:) } + let(:meeting) { create(:meeting, :published, component:) } let(:meeting_path) do decidim_participatory_process_meetings.meeting_path( participatory_process_slug: participatory_process.slug, @@ -32,13 +32,6 @@ id: meeting.id ) end - let(:meeting_live_event_path) do - decidim_participatory_process_meetings.meeting_live_event_path( - participatory_process_slug: participatory_process.slug, - component_id: component.id, - meeting_id: meeting.id - ) - end let(:body_multiple_option_question) do { en: "This is the first question", @@ -56,19 +49,26 @@ let!(:poll) { create(:poll, meeting:) } let!(:questionnaire) { create(:meetings_poll_questionnaire, questionnaire_for: poll) } - before do - visit meeting_live_event_path - click_on "Administrate" - end - context "when all questions are unpublished" do let!(:question_multiple_option) { create(:meetings_poll_question, :unpublished, questionnaire:, body: body_multiple_option_question, question_type: "multiple_option") } let!(:question_single_option) { create(:meetings_poll_question, :unpublished, questionnaire:, body: body_single_option_question, question_type: "single_option") } + before do + visit meeting_path + within("[aria-label='aside']") do + click_link_or_button "Reply poll" + end + click_link_or_button "Administrate" + end + it "list the questions in the Administrate section" do expect(page.all(".meeting-polls__question--admin").size).to eq(2) end + it "shows the status of each question" do + expect(page).to have_content("Pending to be sent", count: 2) + end + it "allows to edit a question in the administrator" do open_first_question @@ -88,6 +88,7 @@ expect(page).to have_content("Sent") expect(page).to have_content("0 received answers") end + expect(page).to have_css("[data-question='#{question_multiple_option.id}']", text: "Sent (open)") end end @@ -100,6 +101,15 @@ let!(:answer_choice_user1) { create(:meetings_poll_answer_choice, answer: answer_user1, answer_option: question_multiple_option.answer_options.first) } let!(:answer_choice_user2) { create(:meetings_poll_answer_choice, answer: answer_user2, answer_option: question_multiple_option.answer_options.first) } + before do + visit meeting_path + within("[aria-label='aside']") do + click_link_or_button "Reply poll" + end + + click_link_or_button "Administrate" + end + it "allows to see question answers" do open_first_question @@ -107,6 +117,10 @@ expect(page).to have_content("100%") end + it "shows the status of each question" do + expect(page).to have_content("Sent (open)", count: 1) + end + it "allows to close a published question" do open_first_question @@ -117,6 +131,7 @@ question_multiple_option.reload expect(question_multiple_option).to be_closed + expect(page).to have_css("[data-question='#{question_multiple_option.id}']", text: "Results sent (closed)") end end @@ -127,6 +142,6 @@ def questionnaire_edit_path end def open_first_question - page.first(".meeting-polls__question--admin").click + find(".meeting-polls__question--admin", match: :first).click end end diff --git a/decidim-meetings/spec/system/live_meeting_answer_questions_spec.rb b/decidim-meetings/spec/system/meeting_questions_reply_poll_spec.rb similarity index 77% rename from decidim-meetings/spec/system/live_meeting_answer_questions_spec.rb rename to decidim-meetings/spec/system/meeting_questions_reply_poll_spec.rb index 81af649d72128..b2081aea1a354 100644 --- a/decidim-meetings/spec/system/live_meeting_answer_questions_spec.rb +++ b/decidim-meetings/spec/system/meeting_questions_reply_poll_spec.rb @@ -2,22 +2,22 @@ require "spec_helper" -describe "Meeting live event poll answer" do +describe "Meeting poll answer" do include_context "with a component" let(:manifest_name) { "meetings" } - let(:user2) do + let(:user) do create(:user, :confirmed, organization:) end let(:meeting) { create(:meeting, :published, :online, :live, component:) } - let(:meeting_live_event_path) do - decidim_participatory_process_meetings.meeting_live_event_path( + let(:meeting_path) do + decidim_participatory_process_meetings.meeting_path( participatory_process_slug: participatory_process.slug, component_id: component.id, - meeting_id: meeting.id + id: meeting.id ) end let(:body_multiple_option_question) do @@ -37,9 +37,13 @@ let!(:poll) { create(:poll, meeting:) } let!(:questionnaire) { create(:meetings_poll_questionnaire, questionnaire_for: poll) } - before do - login_as user, scope: :user - visit meeting_live_event_path + context "when there are no questions" do + it "does not show a link to reply poll" do + login_as user, scope: :user + visit meeting_path + + expect(page).to have_no_content("Reply poll") + end end context "when all questions are unpublished" do @@ -47,12 +51,16 @@ let!(:question_single_option) { create(:meetings_poll_question, :unpublished, questionnaire:, body: body_single_option_question, question_type: "single_option") } before do - visit meeting_live_event_path + login_as user, scope: :user + visit meeting_path + within("[aria-label='aside']") do + click_link_or_button "Reply poll" + end end it "does not list any question" do - click_on "Questions (0)" expect(page.all(".meeting-polls__question--admin").size).to eq(0) + expect(page).to have_content("some questions will be sent") end end @@ -61,11 +69,15 @@ let!(:question_single_option) { create(:meetings_poll_question, :published, questionnaire:, body: body_single_option_question, question_type: "single_option") } before do - visit meeting_live_event_path + login_as user, scope: :user + visit meeting_path + within("[aria-label='aside']") do + click_link_or_button "Reply poll" + end end it "allows to reply a question" do - click_on "Questions (2)" + sleep(2) open_first_question check question_multiple_option.answer_options.first.body["en"] @@ -75,7 +87,6 @@ end it "does not allow selecting two single options" do - click_on "Questions (2)" find("details[data-question='#{question_single_option.id}']").click choose question_single_option.answer_options.first.body["en"] @@ -88,14 +99,15 @@ end it "does not allow selecting more than the maximum choices for multiple options" do - click_on "Questions (2)" + sleep(2) open_first_question check question_multiple_option.answer_options.first.body["en"] check question_multiple_option.answer_options.second.body["en"] check question_multiple_option.answer_options.third.body["en"] - expect(page).to have_content("There are too many choices selected") + click_on "Reply question" + expect(page).to have_content("You can choose a maximum of 2.") end end @@ -105,11 +117,14 @@ let!(:answer_choice_user1) { create(:meetings_poll_answer_choice, answer: answer_user1) } before do - visit meeting_live_event_path + login_as user, scope: :user + visit meeting_path + within("[aria-label='aside']") do + click_link_or_button "View poll" + end end it "shows the responses" do - click_on "Questions (1)" open_first_question expect(page).to have_content("0%") @@ -124,6 +139,6 @@ def questionnaire_edit_path end def open_first_question - page.first(".meeting-polls__question").click + find(".meeting-polls__question", match: :first).click end end From c144a2b9e49202b0b8c3d4f904d96dc75f44c9df Mon Sep 17 00:00:00 2001 From: Tom <101816158+greenwoodt@users.noreply.github.com> Date: Mon, 17 Jun 2024 08:02:34 +0200 Subject: [PATCH 14/19] Standardise `current_user` call within Commands tasks (Meetings part 2) (#12951) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * updated delegation to form. Refactored join_meeting but failing on user symbol * user: not being recognized * controller concern fix * fixed join_meeting_spec file called user: current_user * .merge added to create method to form in controller * test for merge params * error in syntax * Revert "test for merge params" This reverts commit 695bdabea5f313676c07eb1ae45b55d704e7dad3. * reverting commit * current_user assigned create method in registrations controller * removing commented out code * changes based on Andres' review * Update join_meeting.rb Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Andrés Pereira de Lucena --- .../decidim/forms/answer_questionnaire.rb | 8 ++-- .../forms/concerns/has_questionnaire.rb | 2 +- .../forms/answer_questionnaire_spec.rb | 5 ++- .../commands/decidim/meetings/join_meeting.rb | 37 +++++++++---------- .../meetings/registrations_controller.rb | 6 +-- .../spec/commands/join_meeting_spec.rb | 34 ++++++++++++----- 6 files changed, 54 insertions(+), 38 deletions(-) diff --git a/decidim-forms/app/commands/decidim/forms/answer_questionnaire.rb b/decidim-forms/app/commands/decidim/forms/answer_questionnaire.rb index 235fbdedad569..51505e144bfff 100644 --- a/decidim-forms/app/commands/decidim/forms/answer_questionnaire.rb +++ b/decidim-forms/app/commands/decidim/forms/answer_questionnaire.rb @@ -4,15 +4,15 @@ module Decidim module Forms # This command is executed when the user answers a Questionnaire. class AnswerQuestionnaire < Decidim::Command + delegate :current_user, to: :form include ::Decidim::MultipleAttachmentsMethods # Initializes a AnswerQuestionnaire Command. # # form - The form from which to get the data. # questionnaire - The current instance of the questionnaire to be answered. - def initialize(form, current_user, questionnaire) + def initialize(form, questionnaire) @form = form - @current_user = current_user @questionnaire = questionnaire end @@ -34,7 +34,7 @@ def call end end - attr_reader :form, :questionnaire, :current_user + attr_reader :form, :questionnaire private @@ -82,7 +82,7 @@ def answer_questionnaire Answer.transaction(requires_new: true) do form.responses_by_step.flatten.select(&:display_conditions_fulfilled?).each do |form_answer| answer = Answer.new( - user: @current_user, + user: current_user, questionnaire: @questionnaire, question: form_answer.question, body: form_answer.body, diff --git a/decidim-forms/app/controllers/decidim/forms/concerns/has_questionnaire.rb b/decidim-forms/app/controllers/decidim/forms/concerns/has_questionnaire.rb index 87721b46135ff..9b6a5da9edd6d 100644 --- a/decidim-forms/app/controllers/decidim/forms/concerns/has_questionnaire.rb +++ b/decidim-forms/app/controllers/decidim/forms/concerns/has_questionnaire.rb @@ -30,7 +30,7 @@ def answer @form = form(Decidim::Forms::QuestionnaireForm).from_params(params, session_token:, ip_hash:) - Decidim::Forms::AnswerQuestionnaire.call(@form, current_user, questionnaire) do + Decidim::Forms::AnswerQuestionnaire.call(@form, questionnaire) do on(:ok) do # i18n-tasks-use t("decidim.forms.questionnaires.answer.success") flash[:notice] = I18n.t("answer.success", scope: i18n_flashes_scope) diff --git a/decidim-forms/spec/commands/decidim/forms/answer_questionnaire_spec.rb b/decidim-forms/spec/commands/decidim/forms/answer_questionnaire_spec.rb index a07a5c51a43cc..05a4766f1c4a5 100644 --- a/decidim-forms/spec/commands/decidim/forms/answer_questionnaire_spec.rb +++ b/decidim-forms/spec/commands/decidim/forms/answer_questionnaire_spec.rb @@ -5,12 +5,13 @@ module Decidim module Forms describe AnswerQuestionnaire do - let(:command) { described_class.new(form, current_user, questionnaire) } + let(:command) { described_class.new(form, questionnaire) } let(:form) do QuestionnaireForm.from_params( form_params ).with_context( current_organization:, + current_user:, session_token:, ip_hash: ) @@ -228,7 +229,7 @@ def tokenize(id) "tos_agreement" => "1" } end - let(:command) { described_class.new(form, current_user, questionnaire_conditioned) } + let(:command) { described_class.new(form, questionnaire_conditioned) } it "broadcasts ok" do expect { command.call }.to broadcast(:ok) diff --git a/decidim-meetings/app/commands/decidim/meetings/join_meeting.rb b/decidim-meetings/app/commands/decidim/meetings/join_meeting.rb index 3516e5ab2f8a8..80fa4927022c5 100644 --- a/decidim-meetings/app/commands/decidim/meetings/join_meeting.rb +++ b/decidim-meetings/app/commands/decidim/meetings/join_meeting.rb @@ -4,16 +4,15 @@ module Decidim module Meetings # This command is executed when the user joins a meeting. class JoinMeeting < Decidim::Command + delegate :current_user, to: :form # Initializes a JoinMeeting Command. # # meeting - The current instance of the meeting to be joined. - # user - The user joining the meeting. - # registration_form - A form object with params; can be a questionnaire. - def initialize(meeting, user, registration_form) + # form - A form object with params; can be a questionnaire. + def initialize(meeting, form) @meeting = meeting - @user = user - @user_group = Decidim::UserGroup.find_by(id: registration_form.user_group_id) - @registration_form = registration_form + @user_group = Decidim::UserGroup.find_by(id: form.user_group_id) + @form = form end # Creates a meeting registration if the meeting has registrations enabled @@ -22,7 +21,7 @@ def initialize(meeting, user, registration_form) # Broadcasts :ok if successful, :invalid otherwise. def call return broadcast(:invalid) unless can_join_meeting? - return broadcast(:invalid_form) unless registration_form.valid? + return broadcast(:invalid_form) unless form.valid? return broadcast(:invalid) if answer_questionnaire == :invalid meeting.with_lock do @@ -39,16 +38,16 @@ def call private - attr_reader :meeting, :user, :user_group, :registration, :registration_form + attr_reader :meeting, :user_group, :registration, :form def accept_invitation - meeting.invites.find_by(user:)&.accept! + meeting.invites.find_by(user: current_user)&.accept! end def answer_questionnaire return unless questionnaire? - Decidim::Forms::AnswerQuestionnaire.call(registration_form, user, meeting.questionnaire) do + Decidim::Forms::AnswerQuestionnaire.call(form, meeting.questionnaire) do on(:ok) do return :valid end @@ -62,19 +61,19 @@ def answer_questionnaire def create_registration @registration = Decidim::Meetings::Registration.create!( meeting:, - user:, + user: current_user, user_group:, - public_participation: registration_form.public_participation + public_participation: form.public_participation ) end def can_join_meeting? meeting.registrations_enabled? && meeting.has_available_slots? && - !meeting.has_registration_for?(user) + !meeting.has_registration_for?(current_user) end def send_email_confirmation - Decidim::Meetings::RegistrationMailer.confirmation(user, meeting, registration).deliver_later + Decidim::Meetings::RegistrationMailer.confirmation(current_user, meeting, registration).deliver_later end def send_notification_confirmation @@ -82,7 +81,7 @@ def send_notification_confirmation event: "decidim.events.meetings.meeting_registration_confirmed", event_class: Decidim::Meetings::MeetingRegistrationNotificationEvent, resource: @meeting, - affected_users: [@user], + affected_users: [current_user], extra: { registration_code: @registration.code } @@ -113,17 +112,17 @@ def send_notification_over(percentage) end def increment_score - Decidim::Gamification.increment_score(user, :attended_meetings) + Decidim::Gamification.increment_score(current_user, :attended_meetings) end def follow_meeting - Decidim::CreateFollow.call(follow_form, user) + Decidim::CreateFollow.call(follow_form, current_user) end def follow_form Decidim::FollowForm .from_params(followable_gid: meeting.to_signed_global_id.to_s) - .with_context(current_user: user) + .with_context(current_user:) end def occupied_slots_over?(percentage) @@ -131,7 +130,7 @@ def occupied_slots_over?(percentage) end def questionnaire? - registration_form.model_name == "questionnaire" + form.model_name == "questionnaire" end end end diff --git a/decidim-meetings/app/controllers/decidim/meetings/registrations_controller.rb b/decidim-meetings/app/controllers/decidim/meetings/registrations_controller.rb index e391d0da83c4a..b9582ebf49c64 100644 --- a/decidim-meetings/app/controllers/decidim/meetings/registrations_controller.rb +++ b/decidim-meetings/app/controllers/decidim/meetings/registrations_controller.rb @@ -11,7 +11,7 @@ def answer @form = form(Decidim::Forms::QuestionnaireForm).from_params(params, session_token:) - JoinMeeting.call(meeting, current_user, @form) do + JoinMeeting.call(meeting, @form) do on(:ok) do flash[:notice] = I18n.t("registrations.create.success", scope: "decidim.meetings") redirect_to after_answer_path @@ -32,9 +32,9 @@ def answer def create enforce_permission_to(:register, :meeting, meeting:) - @form = JoinMeetingForm.from_params(params) + @form = JoinMeetingForm.from_params(params).with_context(current_user:) - JoinMeeting.call(meeting, current_user, @form) do + JoinMeeting.call(meeting, @form) do on(:ok) do flash[:notice] = I18n.t("registrations.create.success", scope: "decidim.meetings") redirect_after_path diff --git a/decidim-meetings/spec/commands/join_meeting_spec.rb b/decidim-meetings/spec/commands/join_meeting_spec.rb index bfb9aa1cea3bf..37a5973d4f7d2 100644 --- a/decidim-meetings/spec/commands/join_meeting_spec.rb +++ b/decidim-meetings/spec/commands/join_meeting_spec.rb @@ -4,7 +4,7 @@ module Decidim::Meetings describe JoinMeeting do - subject { described_class.new(meeting, user, registration_form) } + subject { described_class.new(meeting, form) } let(:organization) { create(:organization) } let(:participatory_process) { create(:participatory_process, organization:) } @@ -24,7 +24,23 @@ module Decidim::Meetings let(:user) { create(:user, :confirmed, organization:, notifications_sending_frequency: "none") } - let(:registration_form) { Decidim::Meetings::JoinMeetingForm.new } + let(:command) { described_class.new(form) } + + let(:user_group) { create(:user_group) } + + let(:form_params) do + { + user_group_id: user_group.id + } + end + + let(:form) do + Decidim::Meetings::JoinMeetingForm.from_params( + form_params + ).with_context( + current_user: user + ) + end let(:badge_notification) { hash_including(event: "decidim.events.gamification.badge_earned") } let(:user_notification) do @@ -63,7 +79,7 @@ module Decidim::Meetings context "when the form has public_participation set to true" do before do - registration_form.public_participation = true + form.public_participation = true end it "creates a registration for the meeting and the user with public participation" do @@ -252,7 +268,7 @@ module Decidim::Meetings let!(:questionnaire) { create(:questionnaire) } let!(:question) { create(:questionnaire_question, questionnaire:) } let(:session_token) { "some-token" } - let(:registration_form) { Decidim::Forms::QuestionnaireForm.from_model(questionnaire).with_context(session_token:) } + let(:form) { Decidim::Forms::QuestionnaireForm.from_model(questionnaire).with_context(session_token:, current_user: user) } context "and the registration form is invalid" do it "broadcast invalid_form" do @@ -262,8 +278,8 @@ module Decidim::Meetings context "and everything is ok" do before do - registration_form.tos_agreement = true - registration_form.responses.first.body = "My answer response" + form.tos_agreement = true + form.responses.first.body = "My answer response" end it "broadcasts ok" do @@ -288,9 +304,9 @@ module Decidim::Meetings context "when the form has public_participation set to true" do before do - registration_form.tos_agreement = true - registration_form.responses.first.body = "My answer response" - registration_form.public_participation = true + form.tos_agreement = true + form.responses.first.body = "My answer response" + form.public_participation = true end it "creates a registration for the meeting and the user with public participation" do From 70ae27e4cf7b0a1aed1eddccbea96a7597daf3b0 Mon Sep 17 00:00:00 2001 From: elviabth Date: Wed, 19 Jun 2024 10:18:27 +0200 Subject: [PATCH 15/19] modify warning message for blocked comments --- decidim-comments/config/locales/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-comments/config/locales/en.yml b/decidim-comments/config/locales/en.yml index 98796a0f703bb..d8679a34ca712 100644 --- a/decidim-comments/config/locales/en.yml +++ b/decidim-comments/config/locales/en.yml @@ -104,7 +104,7 @@ en: comments: blocked_comments_for_unauthorized_user_warning: You need to be verified to comment at this moment, but you can read the previous ones. blocked_comments_for_user_warning: You are not able to comment at this moment, but you can read the previous ones. - blocked_comments_warning: Comments are disabled at this time, but you can read the previous ones. + blocked_comments_warning: Comments are currently disabled, only administrators can reply or post new ones. comment_details_title: Comment details loading: Loading comments ... single_comment_warning: View all comments From 4df034c67cbd6bd50d97765f054e91aacb07a6e5 Mon Sep 17 00:00:00 2001 From: elviabth Date: Wed, 19 Jun 2024 15:42:24 +0200 Subject: [PATCH 16/19] enable admin to post comments --- .../decidim/admin/needs_admin_tos_accepted.rb | 31 +-------------- .../cells/decidim/comments/comments_cell.rb | 17 ++++++--- .../forms/decidim/comments/comment_form.rb | 13 +++++++ .../app/models/decidim/comments/comment.rb | 7 ---- .../comments/commentable_with_component.rb | 2 + .../app/cells/decidim/comments_button_cell.rb | 10 ++++- .../concerns/decidim/user_role_checker.rb | 38 +++++++++++++++++++ 7 files changed, 76 insertions(+), 42 deletions(-) create mode 100644 decidim-core/app/helpers/concerns/decidim/user_role_checker.rb diff --git a/decidim-admin/app/controllers/concerns/decidim/admin/needs_admin_tos_accepted.rb b/decidim-admin/app/controllers/concerns/decidim/admin/needs_admin_tos_accepted.rb index eefd1dec119bf..19780589cee58 100644 --- a/decidim-admin/app/controllers/concerns/decidim/admin/needs_admin_tos_accepted.rb +++ b/decidim-admin/app/controllers/concerns/decidim/admin/needs_admin_tos_accepted.rb @@ -7,6 +7,7 @@ module NeedsAdminTosAccepted extend ActiveSupport::Concern included do + include UserRoleChecker before_action :tos_accepted_by_admin end @@ -15,7 +16,7 @@ module NeedsAdminTosAccepted def tos_accepted_by_admin return unless request.format.html? return unless current_user - return unless user_has_any_role? + return unless user_has_any_role?(current_user) return if current_user.admin_terms_accepted? return if permitted_paths? @@ -38,34 +39,6 @@ def permitted_paths def admin_tos_path decidim_admin.admin_terms_show_path end - - def user_has_any_role? - return true if current_user.admin - return true if current_user.roles.any? - return true if participatory_process_user_role? - return true if assembly_user_role? - return true if conference_user_role? - - false - end - - def participatory_process_user_role? - return false unless Decidim.module_installed?(:participatory_processes) - - true if Decidim::ParticipatoryProcessUserRole.exists?(user: current_user) - end - - def assembly_user_role? - return false unless Decidim.module_installed?(:assemblies) - - true if Decidim::AssemblyUserRole.exists?(user: current_user) - end - - def conference_user_role? - return false unless Decidim.module_installed?(:conferences) - - true if Decidim::ConferenceUserRole.exists?(user: current_user) - end end end end diff --git a/decidim-comments/app/cells/decidim/comments/comments_cell.rb b/decidim-comments/app/cells/decidim/comments/comments_cell.rb index af497a65464d2..f9958f35b0174 100644 --- a/decidim-comments/app/cells/decidim/comments/comments_cell.rb +++ b/decidim-comments/app/cells/decidim/comments/comments_cell.rb @@ -4,13 +4,11 @@ module Decidim module Comments # A cell to display a comments section for a commentable object. class CommentsCell < Decidim::ViewModel + include UserRoleChecker delegate :user_signed_in?, to: :controller - def add_comment - return if single_comment? - return if comments_blocked? - return if user_comments_blocked? - render :add_comment + def add_comment + render :add_comment if show_comments? end def single_comment_warning @@ -41,6 +39,15 @@ def user_comments_blocked_warning private + def show_comments? + return true if user_has_any_role?(current_user) + return if single_comment? + return if comments_blocked? + return if user_comments_blocked? + + true + end + def decidim_comments Decidim::Comments::Engine.routes.url_helpers end diff --git a/decidim-comments/app/forms/decidim/comments/comment_form.rb b/decidim-comments/app/forms/decidim/comments/comment_form.rb index 53bc053171612..642803e91d50f 100644 --- a/decidim-comments/app/forms/decidim/comments/comment_form.rb +++ b/decidim-comments/app/forms/decidim/comments/comment_form.rb @@ -5,6 +5,8 @@ module Comments # A form object used to create comments from the graphql api. # class CommentForm < Form + include Decidim::UserRoleChecker + attribute :body, Decidim::Attributes::CleanString attribute :alignment, Integer attribute :user_group_id, Integer @@ -17,6 +19,7 @@ class CommentForm < Form validates :alignment, inclusion: { in: [0, 1, -1] }, if: ->(form) { form.alignment.present? } validate :max_depth + validate :commentable_can_have_comments def max_length if current_component.try(:settings).respond_to?(:comments_max_length) @@ -33,6 +36,16 @@ def max_depth errors.add(:base, :invalid) if commentable.depth >= Comment::MAX_DEPTH end + + private + + # Private: Check if commentable can have comments and if not adds + # a validation error to the model + def commentable_can_have_comments + return if user_has_any_role?(current_user) + + errors.add(:commentable, :cannot_have_comments) unless commentable.accepts_new_comments? + end end end end diff --git a/decidim-comments/app/models/decidim/comments/comment.rb b/decidim-comments/app/models/decidim/comments/comment.rb index 2b6887863bca8..0763579c11d81 100644 --- a/decidim-comments/app/models/decidim/comments/comment.rb +++ b/decidim-comments/app/models/decidim/comments/comment.rb @@ -55,7 +55,6 @@ class Comment < ApplicationRecord validates :depth, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: MAX_DEPTH } validates :alignment, inclusion: { in: [0, 1, -1] } validate :body_length - validate :commentable_can_have_comments scope :not_deleted, -> { where(deleted_at: nil) } @@ -228,12 +227,6 @@ def component_settings_comments_max_length? component.settings.comments_max_length.positive? end - # Private: Check if commentable can have comments and if not adds - # a validation error to the model - def commentable_can_have_comments - errors.add(:commentable, :cannot_have_comments) unless root_commentable.accepts_new_comments? - end - # Private: Compute comment depth inside the current comment tree def compute_depth self.depth = commentable.depth + 1 if commentable.respond_to?(:depth) diff --git a/decidim-comments/lib/decidim/comments/commentable_with_component.rb b/decidim-comments/lib/decidim/comments/commentable_with_component.rb index 00dd3351667e6..90c690ab91756 100644 --- a/decidim-comments/lib/decidim/comments/commentable_with_component.rb +++ b/decidim-comments/lib/decidim/comments/commentable_with_component.rb @@ -9,6 +9,7 @@ module Comments module CommentableWithComponent extend ActiveSupport::Concern include Decidim::Comments::Commentable + include Decidim::UserRoleChecker included do # Public: Overrides the `commentable?` Commentable concern method. @@ -23,6 +24,7 @@ def accepts_new_comments? # Public: Whether the object can have new comments or not. def user_allowed_to_comment?(user) + return true if user_has_any_role?(user) return unless can_participate?(user) ActionAuthorizer.new(user, "comment", component, self).authorize.ok? diff --git a/decidim-core/app/cells/decidim/comments_button_cell.rb b/decidim-core/app/cells/decidim/comments_button_cell.rb index b6b18f3d9fcf3..fe44f69f0bd68 100644 --- a/decidim-core/app/cells/decidim/comments_button_cell.rb +++ b/decidim-core/app/cells/decidim/comments_button_cell.rb @@ -2,6 +2,8 @@ module Decidim class CommentsButtonCell < ButtonCell + include UserRoleChecker + def show if options.has_key?(:display) return render if options[:display] @@ -9,11 +11,17 @@ def show return end - render if component_settings.comments_enabled? && !current_settings.try(:comments_blocked?) + render if comments_enabled? end private + def comments_enabled? + return true if user_has_any_role?(current_user) + + component_settings.comments_enabled? && !current_settings.try(:comments_blocked?) + end + def path "#comments" end diff --git a/decidim-core/app/helpers/concerns/decidim/user_role_checker.rb b/decidim-core/app/helpers/concerns/decidim/user_role_checker.rb new file mode 100644 index 0000000000000..14ae7c3b1f64a --- /dev/null +++ b/decidim-core/app/helpers/concerns/decidim/user_role_checker.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Decidim + module UserRoleChecker + # Shared behaviour for signed_in admins + extend ActiveSupport::Concern + + private + + def user_has_any_role?(user) + return true if user.admin + return true if user.roles.any? + return true if participatory_process_user_role? + return true if assembly_user_role? + return true if conference_user_role? + + false + end + + def participatory_process_user_role?(user) + return false unless Decidim.module_installed?(:participatory_processes) + + true if Decidim::ParticipatoryProcessUserRole.exists?(user:) + end + + def assembly_user_role?(user) + return false unless Decidim.module_installed?(:assemblies) + + true if Decidim::AssemblyUserRole.exists?(user:) + end + + def conference_user_role?(user) + return false unless Decidim.module_installed?(:conferences) + + true if Decidim::ConferenceUserRole.exists?(user:) + end + end +end From 5e93e5fe8a737223376ee21e4ddc75305962962a Mon Sep 17 00:00:00 2001 From: elviabth Date: Wed, 19 Jun 2024 15:50:53 +0200 Subject: [PATCH 17/19] add missing argument to user_has_any_role? methods --- .../app/helpers/concerns/decidim/user_role_checker.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/decidim-core/app/helpers/concerns/decidim/user_role_checker.rb b/decidim-core/app/helpers/concerns/decidim/user_role_checker.rb index 14ae7c3b1f64a..4ffbd58d421eb 100644 --- a/decidim-core/app/helpers/concerns/decidim/user_role_checker.rb +++ b/decidim-core/app/helpers/concerns/decidim/user_role_checker.rb @@ -10,9 +10,9 @@ module UserRoleChecker def user_has_any_role?(user) return true if user.admin return true if user.roles.any? - return true if participatory_process_user_role? - return true if assembly_user_role? - return true if conference_user_role? + return true if participatory_process_user_role?(user) + return true if assembly_user_role?(user) + return true if conference_user_role?(user) false end From d89aeaf91ff31b44c64023df732c944feebe1047 Mon Sep 17 00:00:00 2001 From: elviabth Date: Wed, 19 Jun 2024 15:54:54 +0200 Subject: [PATCH 18/19] add a new condition to user_has_any_role? method --- decidim-core/app/helpers/concerns/decidim/user_role_checker.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/decidim-core/app/helpers/concerns/decidim/user_role_checker.rb b/decidim-core/app/helpers/concerns/decidim/user_role_checker.rb index 4ffbd58d421eb..f77b1f832ebb3 100644 --- a/decidim-core/app/helpers/concerns/decidim/user_role_checker.rb +++ b/decidim-core/app/helpers/concerns/decidim/user_role_checker.rb @@ -8,6 +8,7 @@ module UserRoleChecker private def user_has_any_role?(user) + return false unless user return true if user.admin return true if user.roles.any? return true if participatory_process_user_role?(user) From 2335e6854e34a70f06e19c1108582ed2a8926431 Mon Sep 17 00:00:00 2001 From: elviabth Date: Wed, 19 Jun 2024 16:08:54 +0200 Subject: [PATCH 19/19] enable admin to reply close comments --- decidim-comments/app/cells/decidim/comments/comment_cell.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/decidim-comments/app/cells/decidim/comments/comment_cell.rb b/decidim-comments/app/cells/decidim/comments/comment_cell.rb index 5e29f003fc6c1..a514ff885822d 100644 --- a/decidim-comments/app/cells/decidim/comments/comment_cell.rb +++ b/decidim-comments/app/cells/decidim/comments/comment_cell.rb @@ -5,6 +5,7 @@ module Comments # A cell to display a single comment. class CommentCell < Decidim::ViewModel include Decidim::ResourceHelper + include Decidim::UserRoleChecker include Cell::ViewModel::Partial delegate :current_user, :user_signed_in?, to: :controller @@ -95,6 +96,8 @@ def context_menu_id end def can_reply? + return true if user_has_any_role?(current_user) + user_signed_in? && accepts_new_comments? && root_commentable.user_allowed_to_comment?(current_user) end