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]) }