From 08d79b626dad9a2ba0f8d7b4af6b6baeb180ef89 Mon Sep 17 00:00:00 2001 From: Anna Topalidi <60363870+antopalidi@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:08:28 +0100 Subject: [PATCH] Add attachments to debate (#13521) Co-authored-by: Alexandru Emil Lupu --- .../debates/user_creates_debate_spec.rb | 3 + .../decidim/debates/admin/create_debate.rb | 22 +++++ .../decidim/debates/admin/update_debate.rb | 25 +++++ .../commands/decidim/debates/create_debate.rb | 21 ++++ .../commands/decidim/debates/update_debate.rb | 25 +++++ .../debates/admin/debates_controller.rb | 6 +- .../decidim/debates/debates_controller.rb | 7 +- .../decidim/debates/admin/debate_form.rb | 16 ++++ .../app/forms/decidim/debates/debate_form.rb | 6 ++ .../app/models/decidim/debates/debate.rb | 1 + .../debates/admin/debates/_form.html.erb | 13 +++ .../decidim/debates/debates/_form.html.erb | 13 +++ .../decidim/debates/debates/show.html.erb | 2 + decidim-debates/config/locales/en.yml | 4 + .../lib/decidim/debates/component.rb | 1 + .../debates/admin/create_debate_spec.rb | 75 ++++++++++++++- .../debates/admin/update_debate_spec.rb | 74 +++++++++++++- .../decidim/debates/create_debate_spec.rb | 76 ++++++++++++++- .../decidim/debates/update_debate_spec.rb | 95 +++++++++++++++++- .../decidim/debates/admin/debate_form_spec.rb | 43 ++++++++- .../decidim/debates/attachment_form_spec.rb | 82 ++++++++++++++++ .../forms/decidim/debates/debate_form_spec.rb | 24 ++++- .../spec/shared/manage_debates_examples.rb | 96 +++++++++++++++++++ .../spec/system/user_creates_debate_spec.rb | 69 +++++++++++++ .../spec/system/user_edits_debate_spec.rb | 62 +++++++++++- 25 files changed, 850 insertions(+), 11 deletions(-) create mode 100644 decidim-debates/spec/forms/decidim/debates/attachment_form_spec.rb diff --git a/decidim-ai/spec/event_handlers/debates/user_creates_debate_spec.rb b/decidim-ai/spec/event_handlers/debates/user_creates_debate_spec.rb index 00843e58011e2..9c6c32746a077 100644 --- a/decidim-ai/spec/event_handlers/debates/user_creates_debate_spec.rb +++ b/decidim-ai/spec/event_handlers/debates/user_creates_debate_spec.rb @@ -3,6 +3,7 @@ require "spec_helper" describe "User creates debate", type: :system do + let(:attachments) { [] } let(:form) do double( invalid?: false, @@ -10,6 +11,8 @@ description:, user_group_id: nil, taxonomizations:, + add_documents: attachments, + documents: [], current_user: author, current_component: component, current_organization: organization diff --git a/decidim-debates/app/commands/decidim/debates/admin/create_debate.rb b/decidim-debates/app/commands/decidim/debates/admin/create_debate.rb index 96fbbb70a1cab..f3e47d16ef024 100644 --- a/decidim-debates/app/commands/decidim/debates/admin/create_debate.rb +++ b/decidim-debates/app/commands/decidim/debates/admin/create_debate.rb @@ -6,8 +6,27 @@ module Admin # This command is executed when the user creates a Debate from the admin # panel. class CreateDebate < Decidim::Commands::CreateResource + include ::Decidim::MultipleAttachmentsMethods + fetch_form_attributes :taxonomizations, :component, :information_updates, :instructions, :start_time, :end_time, :comments_enabled + def call + return broadcast(:invalid) if invalid? + + if process_attachments? + build_attachments + return broadcast(:invalid) if attachments_invalid? + end + + perform! + broadcast(:ok, resource) + rescue ActiveRecord::RecordInvalid + add_file_attribute_errors! + broadcast(:invalid) + rescue Decidim::Commands::HookError + broadcast(:invalid) + end + protected def resource_class = Decidim::Debates::Debate @@ -27,6 +46,9 @@ def attributes end def run_after_hooks + @attached_to = resource + create_attachments(first_weight: 1) if process_attachments? + Decidim::EventsManager.publish( event: "decidim.events.debates.debate_created", event_class: Decidim::Debates::CreateDebateEvent, diff --git a/decidim-debates/app/commands/decidim/debates/admin/update_debate.rb b/decidim-debates/app/commands/decidim/debates/admin/update_debate.rb index aa81c32636b0b..d64192935a132 100644 --- a/decidim-debates/app/commands/decidim/debates/admin/update_debate.rb +++ b/decidim-debates/app/commands/decidim/debates/admin/update_debate.rb @@ -6,8 +6,27 @@ module Admin # This command is executed when the user changes a Debate from the admin # panel. class UpdateDebate < Decidim::Commands::UpdateResource + include Decidim::MultipleAttachmentsMethods + fetch_form_attributes :taxonomizations, :information_updates, :instructions, :start_time, :end_time, :comments_enabled + def call + return broadcast(:invalid) if invalid? + + if process_attachments? + build_attachments + return broadcast(:invalid) if attachments_invalid? + end + + perform! + broadcast(:ok, resource) + rescue ActiveRecord::RecordInvalid + add_file_attribute_errors! + broadcast(:invalid) + rescue Decidim::Commands::HookError + broadcast(:invalid) + end + private def attributes @@ -19,6 +38,12 @@ def attributes description: parsed_description }) end + + def run_after_hooks + @attached_to = resource + document_cleanup!(include_all_attachments: true) + create_attachments(first_weight: 1) if process_attachments? + end end end end diff --git a/decidim-debates/app/commands/decidim/debates/create_debate.rb b/decidim-debates/app/commands/decidim/debates/create_debate.rb index 3ae7578ae5712..a2e9839f23b93 100644 --- a/decidim-debates/app/commands/decidim/debates/create_debate.rb +++ b/decidim-debates/app/commands/decidim/debates/create_debate.rb @@ -5,8 +5,27 @@ module Debates # This command is executed when the user creates a Debate from the public # views. class CreateDebate < Decidim::Commands::CreateResource + include ::Decidim::MultipleAttachmentsMethods + fetch_form_attributes :taxonomizations + def call + return broadcast(:invalid) if invalid? + + if process_attachments? + build_attachments + return broadcast(:invalid) if attachments_invalid? + end + + perform! + broadcast(:ok, resource) + rescue ActiveRecord::RecordInvalid + add_file_attribute_errors! + broadcast(:invalid) + rescue Decidim::Commands::HookError + broadcast(:invalid) + end + private def resource_class = Decidim::Debates::Debate @@ -20,6 +39,8 @@ def create_resource end def run_after_hooks + @attached_to = resource + create_attachments(first_weight: 1) if process_attachments? send_notification_to_author_followers send_notification_to_space_followers follow_debate diff --git a/decidim-debates/app/commands/decidim/debates/update_debate.rb b/decidim-debates/app/commands/decidim/debates/update_debate.rb index e8798caabca08..b2808b1efc7a4 100644 --- a/decidim-debates/app/commands/decidim/debates/update_debate.rb +++ b/decidim-debates/app/commands/decidim/debates/update_debate.rb @@ -4,8 +4,27 @@ module Decidim module Debates # A command with all the business logic when a user updates a debate. class UpdateDebate < Decidim::Commands::UpdateResource + include Decidim::MultipleAttachmentsMethods + fetch_form_attributes :taxonomizations + def call + return broadcast(:invalid) if invalid? + + if process_attachments? + build_attachments + return broadcast(:invalid) if attachments_invalid? + end + + perform! + broadcast(:ok, resource) + rescue ActiveRecord::RecordInvalid + add_file_attribute_errors! + broadcast(:invalid) + rescue Decidim::Commands::HookError + broadcast(:invalid) + end + private def update_resource @@ -34,6 +53,12 @@ def attributes description: { I18n.locale => parsed_description } }) end + + def run_after_hooks + @attached_to = resource + document_cleanup!(include_all_attachments: true) + create_attachments(first_weight: 1) if process_attachments? + end end end end diff --git a/decidim-debates/app/controllers/decidim/debates/admin/debates_controller.rb b/decidim-debates/app/controllers/decidim/debates/admin/debates_controller.rb index 954ff6669e91c..ad9e51531cc47 100644 --- a/decidim-debates/app/controllers/decidim/debates/admin/debates_controller.rb +++ b/decidim-debates/app/controllers/decidim/debates/admin/debates_controller.rb @@ -16,8 +16,9 @@ def index def new enforce_permission_to :create, :debate - - @form = form(Decidim::Debates::Admin::DebateForm).instance + @form = form(Decidim::Debates::Admin::DebateForm).from_params( + attachment: form(AttachmentForm).from_params({}) + ) end def create @@ -40,7 +41,6 @@ def create def edit enforce_permission_to(:update, :debate, debate:) - @form = form(Decidim::Debates::Admin::DebateForm).from_model(debate) end diff --git a/decidim-debates/app/controllers/decidim/debates/debates_controller.rb b/decidim-debates/app/controllers/decidim/debates/debates_controller.rb index 8dcc931a4cf2d..8a159ece39e04 100644 --- a/decidim-debates/app/controllers/decidim/debates/debates_controller.rb +++ b/decidim-debates/app/controllers/decidim/debates/debates_controller.rb @@ -11,8 +11,9 @@ class DebatesController < Decidim::Debates::ApplicationController include Paginable include Flaggable include Decidim::Debates::Orderable + include Decidim::AttachmentsHelper - helper_method :debates, :debate, :form_presenter, :paginated_debates, :close_debate_form + helper_method :debates, :debate, :form_presenter, :paginated_debates, :close_debate_form, :tab_panel_items before_action :authenticate_user!, only: [:new, :create] def new @@ -120,6 +121,10 @@ def default_filter_params with_any_state: %w(open closed) } end + + def tab_panel_items + @tab_panel_items ||= attachments_tab_panel_items(debate) + end end end end diff --git a/decidim-debates/app/forms/decidim/debates/admin/debate_form.rb b/decidim-debates/app/forms/decidim/debates/admin/debate_form.rb index 0b6c7771507d0..209bc6c483890 100644 --- a/decidim-debates/app/forms/decidim/debates/admin/debate_form.rb +++ b/decidim-debates/app/forms/decidim/debates/admin/debate_form.rb @@ -5,6 +5,8 @@ module Debates module Admin # This class holds a Form to create/update debates from Decidim's admin panel. class DebateForm < Decidim::Form + include Decidim::HasUploadValidations + include Decidim::AttachmentAttributes include Decidim::TranslatableAttributes include Decidim::HasTaxonomyFormAttributes @@ -16,6 +18,9 @@ class DebateForm < Decidim::Form attribute :end_time, Decidim::Attributes::TimeWithZone attribute :finite, Boolean, default: true attribute :comments_enabled, Boolean, default: true + attribute :attachment, AttachmentForm + + attachments_attribute :documents validates :title, translatable_presence: true validates :description, translatable_presence: true @@ -23,12 +28,15 @@ class DebateForm < Decidim::Form validates :start_time, presence: { if: :validate_start_time? }, date: { before: :end_time, allow_blank: true, if: :validate_start_time? } validates :end_time, presence: { if: :validate_end_time? }, date: { after: :start_time, allow_blank: true, if: :validate_end_time? } + validate :notify_missing_attachment_if_errored + def map_model(model) self.finite = model.start_time.present? && model.end_time.present? presenter = DebatePresenter.new(model) self.title = presenter.title(all_locales: title.is_a?(Hash)) self.description = presenter.description(all_locales: description.is_a?(Hash)) + self.documents = model.attachments end def participatory_space_manifest @@ -44,6 +52,14 @@ def validate_end_time? def validate_start_time? end_time.present? end + + # This method will add an error to the `add_documents` field only if there is + # any error in any other field. This is needed because when the form has + # an error, the attachment is lost, so we need a way to inform the user of + # this problem. + def notify_missing_attachment_if_errored + errors.add(:add_documents, :needs_to_be_reattached) if errors.any? && add_documents.present? + end end end end diff --git a/decidim-debates/app/forms/decidim/debates/debate_form.rb b/decidim-debates/app/forms/decidim/debates/debate_form.rb index 0d0b0003b1c32..816d9b483cf7b 100644 --- a/decidim-debates/app/forms/decidim/debates/debate_form.rb +++ b/decidim-debates/app/forms/decidim/debates/debate_form.rb @@ -4,12 +4,17 @@ module Decidim module Debates # This class holds a Form to create/update debates from Decidim's public views. class DebateForm < Decidim::Form + include Decidim::HasUploadValidations + include Decidim::AttachmentAttributes include Decidim::TranslatableAttributes include Decidim::HasTaxonomyFormAttributes attribute :title, String attribute :description, String attribute :user_group_id, Integer + attribute :attachment, AttachmentForm + + attachments_attribute :documents validates :title, presence: true validates :description, presence: true @@ -23,6 +28,7 @@ def map_model(debate) self.title = debate.title.values.first self.description = debate.description.values.first self.user_group_id = debate.decidim_user_group_id + self.documents = debate.attachments end def participatory_space_manifest diff --git a/decidim-debates/app/models/decidim/debates/debate.rb b/decidim-debates/app/models/decidim/debates/debate.rb index 3be2e56757400..13037a96f8b2f 100644 --- a/decidim-debates/app/models/decidim/debates/debate.rb +++ b/decidim-debates/app/models/decidim/debates/debate.rb @@ -16,6 +16,7 @@ class Debate < Debates::ApplicationRecord include Decidim::ScopableResource include Decidim::Authorable include Decidim::Reportable + include Decidim::HasAttachments include Decidim::HasReference include Decidim::Traceable include Decidim::Loggable diff --git a/decidim-debates/app/views/decidim/debates/admin/debates/_form.html.erb b/decidim-debates/app/views/decidim/debates/admin/debates/_form.html.erb index bf1a7fe46523b..b5534468ce952 100644 --- a/decidim-debates/app/views/decidim/debates/admin/debates/_form.html.erb +++ b/decidim-debates/app/views/decidim/debates/admin/debates/_form.html.erb @@ -45,6 +45,19 @@
<%= form.check_box :comments_enabled, label: t("enabled", scope: "decidim.comments.admin.shared.availability_fields") %>
+ + <% if component_settings.attachments_allowed? %> +
+ <%= form.attachment :documents, + multiple: true, + label: t("decidim.debates.admin.debates.form.add_documents"), + button_label: t("decidim.debates.admin.debates.form.add_documents"), + button_edit_label: t("decidim.debates.admin.debates.form.edit_documents"), + button_class: "button button__sm button__transparent-secondary", + help_i18n_scope: "decidim.forms.file_help.file", + help_text: t("decidim.debates.admin.debates.form.attachment_legend") %> +
+ <% end %> diff --git a/decidim-debates/app/views/decidim/debates/debates/_form.html.erb b/decidim-debates/app/views/decidim/debates/debates/_form.html.erb index ba6fd837e5c73..e1d48feb7669c 100644 --- a/decidim-debates/app/views/decidim/debates/debates/_form.html.erb +++ b/decidim-debates/app/views/decidim/debates/debates/_form.html.erb @@ -13,4 +13,17 @@ <% if @form.id.blank? && Decidim::UserGroups::ManageableUserGroups.for(current_user).verified.any? %> <%= form.select :user_group_id, Decidim::UserGroups::ManageableUserGroups.for(current_user).verified.map { |g| [g.name, g.id] }, prompt: current_user.name %> <% end %> + + <% if component_settings.attachments_allowed? %> +
+ <%= form.attachment :documents, + multiple: true, + label: t("decidim.debates.admin.debates.form.add_documents"), + button_label: t("decidim.debates.admin.debates.form.add_documents"), + button_edit_label: t("decidim.debates.admin.debates.form.edit_documents"), + button_class: "button button__lg button__transparent-secondary w-full", + help_i18n_scope: "decidim.forms.file_help.file", + help_text: t("decidim.debates.admin.debates.form.attachment_legend") %> +
+ <% end %> diff --git a/decidim-debates/app/views/decidim/debates/debates/show.html.erb b/decidim-debates/app/views/decidim/debates/debates/show.html.erb index d2157527dcd13..554e5db5a36b6 100644 --- a/decidim-debates/app/views/decidim/debates/debates/show.html.erb +++ b/decidim-debates/app/views/decidim/debates/debates/show.html.erb @@ -53,6 +53,8 @@ edit_link( + <%= cell "decidim/tab_panels", tab_panel_items %> + <% if debate.closed? || translated_attribute(debate.instructions).present? || translated_attribute(debate.information_updates).present? %>
<%= cell("decidim/announcement", { title: t("debate_conclusions_are", scope: "decidim.debates.debates.show", date: l(debate.closed_at, format: :decidim_short)), body: simple_format(translated_attribute(debate.conclusions)) }, callout_class: "success") if debate.closed? %> diff --git a/decidim-debates/config/locales/en.yml b/decidim-debates/config/locales/en.yml index 8db0494a5f5a1..dad46111a78c5 100644 --- a/decidim-debates/config/locales/en.yml +++ b/decidim-debates/config/locales/en.yml @@ -33,6 +33,7 @@ en: settings: global: announcement: Announcement + attachments_allowed: Allow attachments clear_all: Clear all comments_enabled: Comments enabled comments_max_length: Comments max length (Leave 0 for default value) @@ -70,7 +71,10 @@ en: title: Edit debate update: Update debate form: + add_documents: Add documents + attachment_legend: "(Optional) Add an attachment" debate_type: Debate type + edit_documents: Edit documents finite: Finite (With start and end times) open: Open (No start or end times) index: diff --git a/decidim-debates/lib/decidim/debates/component.rb b/decidim-debates/lib/decidim/debates/component.rb index 51ab84241e293..75601811282ea 100644 --- a/decidim-debates/lib/decidim/debates/component.rb +++ b/decidim-debates/lib/decidim/debates/component.rb @@ -23,6 +23,7 @@ settings.attribute :comments_enabled, type: :boolean, default: true settings.attribute :comments_max_length, type: :integer, required: true settings.attribute :announcement, type: :text, translated: true, editor: true + settings.attribute :attachments_allowed, type: :boolean, default: false end component.settings(:step) do |settings| diff --git a/decidim-debates/spec/commands/decidim/debates/admin/create_debate_spec.rb b/decidim-debates/spec/commands/decidim/debates/admin/create_debate_spec.rb index 819bb3132ba4f..a3da52a2b2dc8 100644 --- a/decidim-debates/spec/commands/decidim/debates/admin/create_debate_spec.rb +++ b/decidim-debates/spec/commands/decidim/debates/admin/create_debate_spec.rb @@ -9,6 +9,7 @@ let(:participatory_process) { create(:participatory_process, organization:) } let(:current_component) { create(:component, participatory_space: participatory_process, manifest_name: "debates") } let(:user) { create(:user, :admin, :confirmed, organization:) } + let(:attachments) { [] } let(:taxonomizations) do 2.times.map { build(:taxonomization, taxonomy: create(:taxonomy, :with_parent, organization:), taxonomizable: nil) } end @@ -27,7 +28,10 @@ component: current_component, current_organization: organization, finite:, - comments_enabled: true + comments_enabled: true, + add_documents: attachments, + documents: [], + errors: ActiveModel::Errors.new(self) ) end let(:finite) { true } @@ -120,4 +124,73 @@ end end end + + describe "when debate with attachments" do + let(:debate) { Decidim::Debates::Debate.last } + let(:current_component) { create(:component, participatory_space: participatory_process, manifest_name: "debates", settings: { "attachments_allowed" => true }) } + let(:attachments) do + [ + { file: upload_test_file(Decidim::Dev.asset("city.jpeg"), content_type: "image/jpeg") }, + { file: upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") } + ] + end + + it "creates the debate with attachments" do + expect { subject.call }.to change(Decidim::Debates::Debate, :count).by(1) & change(Decidim::Attachment, :count).by(2) + expect(debate.attachments.map(&:weight)).to eq([1, 2]) + + debate_attachments = debate.attachments + expect(debate_attachments.count).to eq(2) + end + + context "when attachments are invalid" do + let(:attachments) do + [ + { file: upload_test_file(Decidim::Dev.test_file("participatory_text.odt", "application/vnd.oasis.opendocument.text")) } + ] + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + expect { subject.call }.not_to change(Decidim::Debates::Debate, :count) + expect { subject.call }.not_to change(Decidim::Attachment, :count) + end + end + + context "when ActiveRecord::RecordInvalid is raised" do + before do + allow(Decidim::Debates::Debate).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(Decidim::Debates::Debate.new)) + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + end + + it "does not create a debate" do + expect do + subject.call + end.not_to change(Decidim::Debates::Debate, :count) + end + end + + context "when Decidim::Commands::HookError is raised" do + subject { command_instance } + + let(:command_instance) { described_class.new(form) } + + before do + allow(command_instance).to receive(:perform!).and_raise(Decidim::Commands::HookError) + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + end + + it "does not create a debate" do + expect do + subject.call + end.not_to change(Decidim::Debates::Debate, :count) + end + end + end end diff --git a/decidim-debates/spec/commands/decidim/debates/admin/update_debate_spec.rb b/decidim-debates/spec/commands/decidim/debates/admin/update_debate_spec.rb index 010b1c72c1243..2de87613dab9e 100644 --- a/decidim-debates/spec/commands/decidim/debates/admin/update_debate_spec.rb +++ b/decidim-debates/spec/commands/decidim/debates/admin/update_debate_spec.rb @@ -8,6 +8,9 @@ let(:debate) { create(:debate) } let(:organization) { debate.component.organization } let(:user) { create(:user, :admin, :confirmed, organization:) } + let(:attachment_params) { nil } + let(:current_files) { [] } + let(:uploaded_files) { [] } let(:taxonomizations) do 2.times.map { build(:taxonomization, taxonomy: create(:taxonomy, :with_parent, organization:), taxonomizable: nil) } end @@ -23,7 +26,11 @@ end_time: 1.day.from_now + 1.hour, taxonomizations:, current_organization: organization, - comments_enabled: true + comments_enabled: true, + attachment: attachment_params, + add_documents: uploaded_files, + documents: current_files, + errors: ActiveModel::Errors.new(self) ) end let(:invalid) { false } @@ -62,4 +69,69 @@ expect(action_log.version.event).to eq "update" end end + + describe "when debate with attachments" do + let!(:attachment1) { create(:attachment, attached_to: debate, weight: 1, file: file1) } + let!(:attachment2) { create(:attachment, attached_to: debate, weight: 2, file: file2) } + let(:file1) { upload_test_file(Decidim::Dev.asset("city.jpeg"), content_type: "image/jpeg") } + let(:file2) { upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") } + let(:file3) { upload_test_file(Decidim::Dev.asset("city2.jpeg"), content_type: "image/jpeg") } + let(:current_files) { [attachment1, attachment2] } + let(:uploaded_files) { [{ file: file3 }] } + + it "updates the debate with attachments" do + expect(debate.attachments.count).to eq(2) + expect(debate.attachments.map(&:weight)).to eq([1, 2]) + + expect do + subject.call + debate.reload + end.to change(debate.attachments, :count).by(1) + + expect(debate.attachments.count).to eq(3) + expect(debate.attachments.map(&:weight)).to eq([1, 2, 3]) + end + + context "when attachments are invalid" do + let(:uploaded_files) do + [ + { file: upload_test_file(Decidim::Dev.test_file("participatory_text.odt", "application/vnd.oasis.opendocument.text")) } + ] + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + expect { subject.call }.not_to change(Decidim::Debates::Debate, :count) + expect { subject.call }.not_to change(Decidim::Attachment, :count) + end + end + + context "when ActiveRecord::RecordInvalid is raised" do + before do + allow(debate).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(debate)) + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + end + + it "does not update the debate" do + expect(debate.title.except("machine_translations").values.uniq).not_to eq ["title"] + end + end + + context "when Decidim::Commands::HookError is raised" do + subject { command_instance } + + let(:command_instance) { described_class.new(form, debate) } + + before do + allow(command_instance).to receive(:perform!).and_raise(Decidim::Commands::HookError) + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + end + end + end end diff --git a/decidim-debates/spec/commands/decidim/debates/create_debate_spec.rb b/decidim-debates/spec/commands/decidim/debates/create_debate_spec.rb index 3b1ac6e49dab4..e687fcadf8464 100644 --- a/decidim-debates/spec/commands/decidim/debates/create_debate_spec.rb +++ b/decidim-debates/spec/commands/decidim/debates/create_debate_spec.rb @@ -9,6 +9,7 @@ let(:participatory_process) { create(:participatory_process, organization:) } let(:current_component) { create(:component, participatory_space: participatory_process, manifest_name: "debates") } let(:user) { create(:user, organization:) } + let(:attachments) { [] } let(:taxonomizations) do 2.times.map { build(:taxonomization, taxonomy: create(:taxonomy, :with_parent, organization:), taxonomizable: nil) } end @@ -21,7 +22,10 @@ taxonomizations:, current_user: user, current_component:, - current_organization: organization + current_organization: organization, + add_documents: attachments, + documents: [], + errors: ActiveModel::Errors.new(self) ) end let(:invalid) { false } @@ -105,6 +109,76 @@ end end + context "when everything is ok with attachments" do + let(:attachments) do + [ + { file: upload_test_file(Decidim::Dev.test_file("city.jpeg", "image/jpeg")) }, + { file: upload_test_file(Decidim::Dev.test_file("Exampledocument.pdf", "application/pdf")) } + ] + end + + let(:debate) { Decidim::Debates::Debate.last } + + it "creates the debate with attachments" do + expect { subject.call }.to change(Decidim::Debates::Debate, :count).by(1) & change(Decidim::Attachment, :count).by(2) + expect(debate.attachments.map(&:weight)).to eq([1, 2]) + + debate_attachments = debate.attachments + expect(debate_attachments.count).to eq(2) + expect(debate_attachments.map(&:file).map(&:filename).map(&:to_s)).to contain_exactly("city.jpeg", "Exampledocument.pdf") + end + end + + context "when attachments are invalid" do + let(:attachments) do + [ + { file: upload_test_file(Decidim::Dev.test_file("participatory_text.odt", "application/vnd.oasis.opendocument.text")) } + ] + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + expect { subject.call }.not_to change(Decidim::Debates::Debate, :count) + expect { subject.call }.not_to change(Decidim::Attachment, :count) + end + end + + describe "when ActiveRecord::RecordInvalid is raised" do + before do + allow(Decidim::Debates::Debate).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(Decidim::Debates::Debate.new)) + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + end + + it "does not create a debate" do + expect do + subject.call + end.not_to change(Decidim::Debates::Debate, :count) + end + end + + describe "when Decidim::Commands::HookError is raised" do + subject { command_instance } + + let(:command_instance) { described_class.new(form) } + + before do + allow(command_instance).to receive(:perform!).and_raise(Decidim::Commands::HookError) + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + end + + it "does not create a debate" do + expect do + subject.call + end.not_to change(Decidim::Debates::Debate, :count) + end + end + describe "events" do let(:author_follower) { create(:user, organization:) } let!(:author_follow) { create(:follow, followable: user, user: author_follower) } diff --git a/decidim-debates/spec/commands/decidim/debates/update_debate_spec.rb b/decidim-debates/spec/commands/decidim/debates/update_debate_spec.rb index 834de82764cc6..aa3ac866c22fb 100644 --- a/decidim-debates/spec/commands/decidim/debates/update_debate_spec.rb +++ b/decidim-debates/spec/commands/decidim/debates/update_debate_spec.rb @@ -11,11 +11,15 @@ let(:user) { create(:user, organization:) } let(:author) { user } let!(:debate) { create(:debate, author:, component: current_component) } + let(:current_files) { debate.attachments } + let(:uploaded_files) { [] } let(:taxonomies) { create_list(:taxonomy, 2, :with_parent, organization:) } let(:form) do Decidim::Debates::DebateForm.from_params( title: "title", description: "description", + documents: current_files, + add_documents: uploaded_files, taxonomies:, id: debate.id ).with_context( @@ -58,7 +62,7 @@ end end - context "when everything is ok" do + describe "when everything is ok" do it "updates the debate" do expect do subject.call @@ -107,4 +111,93 @@ expect(action_log.version.event).to eq "update" end end + + describe "when debate with attachments" do + let(:current_component) { create(:component, participatory_space: participatory_process, manifest_name: "debates", settings: { "attachments_allowed" => true }) } + let(:uploaded_files) do + [ + { file: upload_test_file(Decidim::Dev.asset("city.jpeg"), content_type: "image/jpeg") }, + { file: upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") } + ] + end + + it "updates the debate with attachments" do + expect do + subject.call + debate.reload + end.to change(debate.attachments, :count).by(2) + + debate_attachments = debate.attachments + expect(debate_attachments.count).to eq(2) + end + + context "when attachments are invalid" do + let(:uploaded_files) do + [ + { file: upload_test_file(Decidim::Dev.test_file("participatory_text.odt", "application/vnd.oasis.opendocument.text")) } + ] + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + expect { subject.call }.not_to change(Decidim::Debates::Debate, :count) + expect { subject.call }.not_to change(Decidim::Attachment, :count) + end + end + end + + describe "when debate already has attachments" do + let!(:attachment1) { create(:attachment, attached_to: debate, weight: 1, file: file1) } + let!(:attachment2) { create(:attachment, attached_to: debate, weight: 2, file: file2) } + let(:file1) { upload_test_file(Decidim::Dev.asset("city.jpeg"), content_type: "image/jpeg") } + let(:file2) { upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") } + let(:file3) { upload_test_file(Decidim::Dev.asset("city2.jpeg"), content_type: "image/jpeg") } + + let(:uploaded_files) do + [ + { file: file3 } + ] + end + + it "adds new attachments and calculates correct weights" do + expect(debate.attachments.count).to eq(2) + expect(debate.attachments.map(&:weight)).to eq([1, 2]) + + expect do + subject.call + debate.reload + end.to change(debate.attachments, :count).by(1) + + expect(debate.attachments.count).to eq(3) + expect(debate.attachments.map(&:weight)).to eq([1, 2, 3]) + end + end + + describe "when ActiveRecord::RecordInvalid is raised" do + before do + allow(debate).to receive(:update!).and_raise(ActiveRecord::RecordInvalid.new(debate)) + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + end + + it "does not update the debate" do + expect(debate.title.except("machine_translations").values.uniq).not_to eq ["title"] + end + end + + describe "when Decidim::Commands::HookError is raised" do + subject { command_instance } + + let(:command_instance) { described_class.new(form, debate) } + + before do + allow(command_instance).to receive(:perform!).and_raise(Decidim::Commands::HookError) + end + + it "broadcasts invalid" do + expect { subject.call }.to broadcast(:invalid) + end + end end diff --git a/decidim-debates/spec/forms/decidim/debates/admin/debate_form_spec.rb b/decidim-debates/spec/forms/decidim/debates/admin/debate_form_spec.rb index eb85f13daa6c6..51dbf8760d63f 100644 --- a/decidim-debates/spec/forms/decidim/debates/admin/debate_form_spec.rb +++ b/decidim-debates/spec/forms/decidim/debates/admin/debate_form_spec.rb @@ -26,6 +26,8 @@ end let(:start_time) { 2.days.from_now } let(:end_time) { 2.days.from_now + 4.hours } + let(:uploaded_files) { [] } + let(:current_files) { [] } let(:taxonomies) { [] } let(:attributes) do { @@ -34,7 +36,9 @@ description:, instructions:, start_time:, - end_time: + end_time:, + add_documents: uploaded_files, + documents: current_files } end @@ -102,16 +106,53 @@ it { is_expected.not_to be_valid } end + describe "when handling attachments" do + let(:uploaded_files) do + [ + { file: upload_test_file(Decidim::Dev.asset("city.jpeg"), content_type: "image/jpeg") }, + { file: upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") } + ] + end + + it "accepts valid attachments" do + expect(form).to be_valid + expect(form.add_documents.count).to eq(2) + end + + context "when an attachment is invalid" do + let(:uploaded_files) do + [ + { file: upload_test_file(Decidim::Dev.asset("invalid_extension.log"), content_type: "text/plain") } + ] + end + + it "does not add the invalid file to the form" do + expect(form.documents).to be_empty + end + end + end + describe "from model" do subject { described_class.from_model(debate).with_context(context) } let(:component) { create(:debates_component) } let(:debate) { create(:debate, component:) } + let!(:attachments) do + [ + create(:attachment, attached_to: debate, title: { en: "Document 1" }), + create(:attachment, attached_to: debate, title: { en: "Document 2" }) + ] + end it "sets the finite value correctly" do expect(subject.finite).to be(false) end + it "sets the documents correctly" do + expect(subject.documents).to match_array(attachments) + expect(subject.documents.map { |doc| doc.title["en"] }).to contain_exactly("Document 1", "Document 2") + end + context "when the debate has start and end dates" do let(:debate) { create(:debate, :open_ama) } diff --git a/decidim-debates/spec/forms/decidim/debates/attachment_form_spec.rb b/decidim-debates/spec/forms/decidim/debates/attachment_form_spec.rb new file mode 100644 index 0000000000000..4cc1c1c9402eb --- /dev/null +++ b/decidim-debates/spec/forms/decidim/debates/attachment_form_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + describe Decidim::AttachmentForm do + subject(:form) do + described_class.from_params( + attributes + ).with_context( + attached_to:, + current_organization: organization + ) + end + + let(:organization) { create(:organization) } + let(:component) { create(:debates_component, organization:) } + let(:attached_to) { create(:debate, component:) } + let(:file) { Decidim::Dev.test_file("city.jpeg", "image/jpeg") } + + context "when everything is ok" do + let(:attributes) do + { + "attachment" => { + "file" => file, + "title" => "Valid Title" + } + } + end + + it { is_expected.to be_valid } + end + + context "without a file" do + let(:attributes) do + { + "attachment" => { + "title" => "Title Without File" + } + } + end + + it { is_expected.to be_valid } + end + + context "when the title is not present" do + let(:attributes) do + { + "attachment" => { + "file" => file + } + } + end + + it { is_expected.not_to be_valid } + end + + context "when the file is not present" do + let(:attributes) do + { + "attachment" => {} + } + end + + it { is_expected.to be_valid } + end + + context "with an invalid file type" do + let(:invalid_file) { Decidim::Dev.test_file("participatory_text.odt", "application/vnd.oasis.opendocument.text") } + let(:attributes) do + { + "attachment" => { + "file" => invalid_file, + "title" => "Title With Invalid File" + } + } + end + + it { is_expected.not_to be_valid } + end + end +end diff --git a/decidim-debates/spec/forms/decidim/debates/debate_form_spec.rb b/decidim-debates/spec/forms/decidim/debates/debate_form_spec.rb index 7ff5561155bc2..44558c1e02286 100644 --- a/decidim-debates/spec/forms/decidim/debates/debate_form_spec.rb +++ b/decidim-debates/spec/forms/decidim/debates/debate_form_spec.rb @@ -17,12 +17,16 @@ let(:current_component) { create(:component, participatory_space: participatory_process, manifest_name: "debates") } let(:title) { "My title" } let(:description) { "My description" } + let(:uploaded_files) { [] } + let(:current_files) { [] } let(:taxonomies) { [] } let(:attributes) do { taxonomies:, title:, - description: + description:, + add_documents: uploaded_files, + documents: current_files } end @@ -65,6 +69,20 @@ end end + context "when handling attachments" do + let(:uploaded_files) do + [ + { file: upload_test_file(Decidim::Dev.asset("city.jpeg"), content_type: "image/jpeg") }, + { file: upload_test_file(Decidim::Dev.asset("Exampledocument.pdf"), content_type: "application/pdf") } + ] + end + + it "accepts valid attachments" do + expect(form).to be_valid + expect(form.add_documents.count).to eq(2) + end + end + describe "map_model" do subject { described_class.from_model(debate).with_context(context) } @@ -81,5 +99,9 @@ it "sets the debate" do expect(subject.debate).to eq(debate) end + + it "sets the attachments" do + expect(subject.documents).to eq(debate.documents) + end end end diff --git a/decidim-debates/spec/shared/manage_debates_examples.rb b/decidim-debates/spec/shared/manage_debates_examples.rb index 54dae32b8609d..b1ad8da8dc73d 100644 --- a/decidim-debates/spec/shared/manage_debates_examples.rb +++ b/decidim-debates/spec/shared/manage_debates_examples.rb @@ -164,6 +164,102 @@ expect(page).to have_content("created the #{translated(attributes[:title])} debate on the") end + describe "Attachments in a debate" do + let(:image_filename) { "city2.jpeg" } + let(:image_path) { Decidim::Dev.asset(image_filename) } + let(:document_filename) { "Exampledocument.pdf" } + let(:document_path) { Decidim::Dev.asset(document_filename) } + let(:invalid_document) { Decidim::Dev.asset("invalid_extension.log") } + + before do + component_settings = current_component["settings"]["global"].merge!(attachments_allowed: true) + current_component.update!(settings: component_settings) + end + + context "when creating a debate with attachments" do + before do + click_on "New debate" + end + + it "creates a new debate with attachments" do + within ".new_debate" do + fill_in_i18n(:debate_title, "#debate-title-tabs", **attributes[:title].except("machine_translations")) + fill_in_i18n_editor(:debate_description, "#debate-description-tabs", **attributes[:description].except("machine_translations")) + fill_in_i18n_editor(:debate_instructions, "#debate-instructions-tabs", **attributes[:instructions].except("machine_translations")) + + choose "Open" + end + + dynamically_attach_file(:debate_documents, image_path) + dynamically_attach_file(:debate_documents, document_path) + + within ".new_debate" do + find("*[type=submit]").click + end + + expect(page).to have_admin_callout "Debate successfully created" + + within "tr[data-id=\"#{Decidim::Debates::Debate.last.id}\"]" do + click_on "Edit" + end + + expect(page).to have_css("img[src*='#{image_filename}']") + expect(page).to have_content(document_filename) + end + + it "shows validation error when format is not accepted" do + dynamically_attach_file(:debate_documents, invalid_document, keep_modal_open: true) do + expect(page).to have_content("Accepted formats: #{Decidim::OrganizationSettings.for(organization).upload_allowed_file_extensions.join(", ")}") + end + expect(page).to have_content("Validation error!") + end + end + + context "when editing a debate with attachments" do + before do + within "tr[data-id=\"#{debate.id}\"]" do + click_on "Edit" + end + end + + it "updates the debate with new attachments", :slow do + within ".edit_debate" do + fill_in_i18n(:debate_title, "#debate-title-tabs", **attributes[:title].except("machine_translations")) + fill_in_i18n_editor(:debate_description, "#debate-description-tabs", **attributes[:description].except("machine_translations")) + fill_in_i18n_editor(:debate_instructions, "#debate-instructions-tabs", **attributes[:instructions].except("machine_translations")) + end + + dynamically_attach_file(:debate_documents, image_path) + dynamically_attach_file(:debate_documents, document_path) + + within ".edit_debate" do + find("*[type=submit]").click + end + + expect(page).to have_admin_callout "Debate successfully updated" + + within "tr[data-id=\"#{debate.id}\"]" do + click_on "Edit" + end + + expect(page).to have_css("img[src*='#{image_filename}']") + expect(page).to have_content(document_filename) + end + end + + context "when attachments are not allowed" do + before do + component_settings = current_component["settings"]["global"].merge!(attachments_allowed: false) + current_component.update!(settings: component_settings) + click_on "New debate" + end + + it "does not show the attachments form", :slow do + expect(page).to have_no_css("#debate_documents_button") + end + end + end + describe "closing a debate", versioning: true do it "closes a debate" do within "tr", text: translated(debate.title) do diff --git a/decidim-debates/spec/system/user_creates_debate_spec.rb b/decidim-debates/spec/system/user_creates_debate_spec.rb index ba562f023871f..70f03f57303e4 100644 --- a/decidim-debates/spec/system/user_creates_debate_spec.rb +++ b/decidim-debates/spec/system/user_creates_debate_spec.rb @@ -29,6 +29,75 @@ settings: { taxonomy_filters: [taxonomy_filter.id] }) end + context "and attachments are not allowed" do + before do + component_settings = component["settings"]["global"].merge!(attachments_allowed: false) + component.update!(settings: component_settings) + visit_component + click_on "New debate" + end + + it "does not show the attachments form" do + expect(page).to have_no_css("#debate_documents_button") + end + end + + context "and attachments are allowed" do + let(:attachments_allowed) { true } + let(:image_filename) { "city2.jpeg" } + let(:image_path) { Decidim::Dev.asset(image_filename) } + let(:document_filename) { "Exampledocument.pdf" } + let(:document_path) { Decidim::Dev.asset(document_filename) } + + before do + component_settings = component["settings"]["global"].merge!(attachments_allowed: true) + component.update!(settings: component_settings) + visit_component + click_on "New debate" + end + + it "creates a new debate" do + within ".new_debate" do + fill_in :debate_title, with: "Should every organization use Decidim?" + fill_in :debate_description, with: "Add your comments on whether Decidim is useful for every organization." + end + + dynamically_attach_file(:debate_documents, image_path) + dynamically_attach_file(:debate_documents, document_path) + + within ".new_debate" do + find("*[type=submit]").click + end + + expect(page).to have_content("successfully") + expect(page).to have_content("Should every organization use Decidim?") + expect(page).to have_content("Add your comments on whether Decidim is useful for every organization.") + expect(page).to have_css("[data-author]", text: user.name) + expect(page).to have_css("img[src*='#{image_filename}']") + + click_on "Documents" + + expect(page).to have_css("a[href*='#{document_filename}']") + expect(page).to have_content("Download file", count: 1) + end + + it "shows validation error when format is not accepted" do + dynamically_attach_file(:debate_documents, Decidim::Dev.asset("dummy-dummies-example.xlsx"), keep_modal_open: true) do + expect(page).to have_content("Accepted formats: #{Decidim::OrganizationSettings.for(organization).upload_allowed_file_extensions.join(", ")}") + end + expect(page).to have_content("Validation error!") + end + + context "when attaching an invalid file format" do + it "shows an error message" do + dynamically_attach_file(:debate_documents, Decidim::Dev.asset("participatory_text.odt"), keep_modal_open: true) do + expect(page).to have_content("Accepted formats: #{Decidim::OrganizationSettings.for(organization).upload_allowed_file_extensions.join(", ")}") + end + expect(page).to have_content("Validation error! Check that the file has an allowed extension or size.") + end + end + end + context "and rich_editor_public_view component setting is enabled" do before do organization.update(rich_text_editor_in_public_views: true) diff --git a/decidim-debates/spec/system/user_edits_debate_spec.rb b/decidim-debates/spec/system/user_edits_debate_spec.rb index 8fda4ad29ee75..077cc6907a32e 100644 --- a/decidim-debates/spec/system/user_edits_debate_spec.rb +++ b/decidim-debates/spec/system/user_edits_debate_spec.rb @@ -6,6 +6,7 @@ include_context "with a component" include_context "with taxonomy filters context" let(:manifest_name) { "debates" } + let(:attachments_allowed) { false } let(:space_manifest) { participatory_process.manifest.name } let(:taxonomies) { [taxonomy] } let!(:debate) do @@ -19,7 +20,7 @@ before do switch_to_host(organization.host) login_as user, scope: :user - component_settings = component["settings"]["global"].merge!(taxonomy_filters: [taxonomy_filter.id]) + component_settings = component["settings"]["global"].merge!(taxonomy_filters: [taxonomy_filter.id], attachments_allowed:) component.update!(settings: component_settings) end @@ -49,6 +50,65 @@ expect(page).to have_css("[data-author]", text: user.name) end + context "when attachments are disallowed" do + it "does not show the attachments form" do + visit_component + + click_on debate.title.values.first + find("#dropdown-trigger-resource-#{debate.id}").click + click_on "Edit" + + expect(page).to have_no_css("#debate_documents_button") + end + end + + context "when attachments are allowed", :slow do + let(:attachments_allowed) { true } + let(:image_filename) { "city2.jpeg" } + let(:image_path) { Decidim::Dev.asset(image_filename) } + let(:document_filename) { "Exampledocument.pdf" } + let(:document_path) { Decidim::Dev.asset(document_filename) } + + before do + visit_component + click_on debate.title.values.first + find("#dropdown-trigger-resource-#{debate.id}").click + click_on "Edit" + end + + it "allows editing my debate", :slow do + within ".edit_debate" do + fill_in :debate_title, with: "Should every organization use Decidim?" + fill_in :debate_description, with: "Add your comments on whether Decidim is useful for every organization." + end + + dynamically_attach_file(:debate_documents, image_path) + dynamically_attach_file(:debate_documents, document_path) + + within ".edit_debate" do + find("*[type=submit]").click + end + + expect(page).to have_content("successfully") + expect(page).to have_css("[data-author]", text: user.name) + expect(page).to have_css("img[src*='#{image_filename}']") + + click_on "Documents" + + expect(page).to have_css("a[href*='#{document_filename}']") + expect(page).to have_content("Download file", count: 1) + end + + context "when attaching an invalid file format" do + it "shows an error message" do + dynamically_attach_file(:debate_documents, Decidim::Dev.asset("participatory_text.odt"), keep_modal_open: true) do + expect(page).to have_content("Accepted formats: #{Decidim::OrganizationSettings.for(organization).upload_allowed_file_extensions.join(", ")}") + end + expect(page).to have_content("Validation error! Check that the file has an allowed extension or size.") + end + end + end + context "when editing as a user group" do let(:author) { user } let!(:user_group) { create(:user_group, :verified, organization:, users: [user]) }