From aef2d6aa93155c220f641dbb7f5f113811ef171f Mon Sep 17 00:00:00 2001
From: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com>
Date: Fri, 18 Oct 2024 11:24:39 +0200
Subject: [PATCH 01/16] fix: Add block reported user task (#614)
---
Gemfile.lock | 37 +++++++++++++++++--------------------
1 file changed, 17 insertions(+), 20 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index f8408c2dbd..2b9b170a0a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -100,7 +100,7 @@ GIT
GIT
remote: https://github.com/OpenSourcePolitics/decidim-module_phone_authorization_handler
- revision: 885122479e7fb9d8294dcf4c4d4f2d34e978b3c6
+ revision: a3e77fb29e9a19793b3ff8b5d2273d41fac0919b
branch: release/0.27-stable
specs:
decidim-phone_authorization_handler (1.0.0)
@@ -108,10 +108,10 @@ GIT
GIT
remote: https://github.com/OpenSourcePolitics/decidim-spam_detection.git
- revision: 5e4f92f19b903228b8349fb002d735e900d63ed4
- tag: 4.1.2
+ revision: 52623c57f571716730532eab8757560818f7f1df
+ tag: 4.1.0
specs:
- decidim-spam_detection (4.1.2)
+ decidim-spam_detection (4.1.0)
decidim-core (~> 0.27.0)
GIT
@@ -151,19 +151,9 @@ GIT
decidim-proposals (~> 0.27)
deface (>= 1.9)
-GIT
- remote: https://github.com/alecslupu-pfa/guest-meeting-registration.git
- revision: 7b3af0d34d053cc430080e483cd6d1e48dcc0f32
- branch: release/0.27-stable
- specs:
- decidim-guest_meeting_registration (0.27.7)
- decidim-core (~> 0.27)
- decidim-meetings (~> 0.27)
- deface (>= 1.9)
-
GIT
remote: https://github.com/decidim-ice/decidim-module-decidim_awesome
- revision: b9aae42bc835485edec5887cb02062caaaf64ed1
+ revision: 51bc593da8fb72d14c2e5b5df55dbb686be1fbbe
branch: release/0.27-stable
specs:
decidim-decidim_awesome (0.10.3)
@@ -426,8 +416,6 @@ GEM
decidim-admin (~> 0.27.0)
decidim-core (~> 0.27.0)
deface (>= 1.9)
- decidim-cleaner (3.1.0)
- decidim-core (~> 0.27.0)
decidim-comments (0.27.4)
decidim-core (= 0.27.4)
redcarpet (~> 3.5, >= 3.5.1)
@@ -1035,6 +1023,14 @@ GEM
semantic_range (3.0.0)
sendgrid-ruby (6.7.0)
ruby_http_client (~> 3.4)
+ sentry-rails (5.16.1)
+ railties (>= 5.0)
+ sentry-ruby (~> 5.16.1)
+ sentry-ruby (5.16.1)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ sentry-sidekiq (5.16.1)
+ sentry-ruby (~> 5.16.1)
+ sidekiq (>= 3.0)
seven_zip_ruby (1.3.0)
sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
@@ -1174,7 +1170,6 @@ DEPENDENCIES
decidim-budgets_booth!
decidim-cache_cleaner
decidim-category_enhanced (~> 0.0.1)
- decidim-cleaner
decidim-conferences (~> 0.27.0)
decidim-custom_proposal_states!
decidim-decidim_awesome!
@@ -1183,7 +1178,6 @@ DEPENDENCIES
decidim-extra_user_fields!
decidim-friendly_signup!
decidim-gallery!
- decidim-guest_meeting_registration!
decidim-half_signup!
decidim-homepage_interactive_map!
decidim-initiatives (~> 0.27.0)
@@ -1214,6 +1208,9 @@ DEPENDENCIES
rack-attack (~> 6.6)
rubocop-faker
sendgrid-ruby
+ sentry-rails
+ sentry-ruby
+ sentry-sidekiq
sidekiq (~> 6.0)
sidekiq-scheduler (~> 5.0)
spring (~> 2.0)
@@ -1226,4 +1223,4 @@ RUBY VERSION
ruby 3.0.6p216
BUNDLED WITH
- 2.5.22
+ 2.4.6
From 6aa15b79f19ca8c6d106d78002ea0d6119200168 Mon Sep 17 00:00:00 2001
From: stephanierousset <61418966+Stef-Rousset@users.noreply.github.com>
Date: Tue, 12 Nov 2024 15:58:29 +0100
Subject: [PATCH 02/16] backport: remove sentry (#622)
---
Gemfile.lock | 15 ++-------------
1 file changed, 2 insertions(+), 13 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 2b9b170a0a..466b4175fc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -153,7 +153,7 @@ GIT
GIT
remote: https://github.com/decidim-ice/decidim-module-decidim_awesome
- revision: 51bc593da8fb72d14c2e5b5df55dbb686be1fbbe
+ revision: b9aae42bc835485edec5887cb02062caaaf64ed1
branch: release/0.27-stable
specs:
decidim-decidim_awesome (0.10.3)
@@ -1023,14 +1023,6 @@ GEM
semantic_range (3.0.0)
sendgrid-ruby (6.7.0)
ruby_http_client (~> 3.4)
- sentry-rails (5.16.1)
- railties (>= 5.0)
- sentry-ruby (~> 5.16.1)
- sentry-ruby (5.16.1)
- concurrent-ruby (~> 1.0, >= 1.0.2)
- sentry-sidekiq (5.16.1)
- sentry-ruby (~> 5.16.1)
- sidekiq (>= 3.0)
seven_zip_ruby (1.3.0)
sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
@@ -1208,9 +1200,6 @@ DEPENDENCIES
rack-attack (~> 6.6)
rubocop-faker
sendgrid-ruby
- sentry-rails
- sentry-ruby
- sentry-sidekiq
sidekiq (~> 6.0)
sidekiq-scheduler (~> 5.0)
spring (~> 2.0)
@@ -1223,4 +1212,4 @@ RUBY VERSION
ruby 3.0.6p216
BUNDLED WITH
- 2.4.6
+ 2.5.10
From 63a25f040cabf9d0add7afb096d5ad2e4da48034 Mon Sep 17 00:00:00 2001
From: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com>
Date: Fri, 15 Nov 2024 11:43:04 +0100
Subject: [PATCH 03/16] bump: Module Spam Detection to 4.1.2 (#630)
---
Gemfile.lock | 24 +++++++++++++++++++-----
1 file changed, 19 insertions(+), 5 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 466b4175fc..f8408c2dbd 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -100,7 +100,7 @@ GIT
GIT
remote: https://github.com/OpenSourcePolitics/decidim-module_phone_authorization_handler
- revision: a3e77fb29e9a19793b3ff8b5d2273d41fac0919b
+ revision: 885122479e7fb9d8294dcf4c4d4f2d34e978b3c6
branch: release/0.27-stable
specs:
decidim-phone_authorization_handler (1.0.0)
@@ -108,10 +108,10 @@ GIT
GIT
remote: https://github.com/OpenSourcePolitics/decidim-spam_detection.git
- revision: 52623c57f571716730532eab8757560818f7f1df
- tag: 4.1.0
+ revision: 5e4f92f19b903228b8349fb002d735e900d63ed4
+ tag: 4.1.2
specs:
- decidim-spam_detection (4.1.0)
+ decidim-spam_detection (4.1.2)
decidim-core (~> 0.27.0)
GIT
@@ -151,6 +151,16 @@ GIT
decidim-proposals (~> 0.27)
deface (>= 1.9)
+GIT
+ remote: https://github.com/alecslupu-pfa/guest-meeting-registration.git
+ revision: 7b3af0d34d053cc430080e483cd6d1e48dcc0f32
+ branch: release/0.27-stable
+ specs:
+ decidim-guest_meeting_registration (0.27.7)
+ decidim-core (~> 0.27)
+ decidim-meetings (~> 0.27)
+ deface (>= 1.9)
+
GIT
remote: https://github.com/decidim-ice/decidim-module-decidim_awesome
revision: b9aae42bc835485edec5887cb02062caaaf64ed1
@@ -416,6 +426,8 @@ GEM
decidim-admin (~> 0.27.0)
decidim-core (~> 0.27.0)
deface (>= 1.9)
+ decidim-cleaner (3.1.0)
+ decidim-core (~> 0.27.0)
decidim-comments (0.27.4)
decidim-core (= 0.27.4)
redcarpet (~> 3.5, >= 3.5.1)
@@ -1162,6 +1174,7 @@ DEPENDENCIES
decidim-budgets_booth!
decidim-cache_cleaner
decidim-category_enhanced (~> 0.0.1)
+ decidim-cleaner
decidim-conferences (~> 0.27.0)
decidim-custom_proposal_states!
decidim-decidim_awesome!
@@ -1170,6 +1183,7 @@ DEPENDENCIES
decidim-extra_user_fields!
decidim-friendly_signup!
decidim-gallery!
+ decidim-guest_meeting_registration!
decidim-half_signup!
decidim-homepage_interactive_map!
decidim-initiatives (~> 0.27.0)
@@ -1212,4 +1226,4 @@ RUBY VERSION
ruby 3.0.6p216
BUNDLED WITH
- 2.5.10
+ 2.5.22
From 3a1bd49e59e009edee61289769b0e7960da2c51d Mon Sep 17 00:00:00 2001
From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com>
Date: Fri, 22 Nov 2024 14:26:42 +0100
Subject: [PATCH 04/16] bump: Fix geocofing on homepage interactive map (#635)
---
Gemfile.lock | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index f8408c2dbd..2364e94449 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -52,10 +52,10 @@ GIT
GIT
remote: https://github.com/OpenSourcePolitics/decidim-module-homepage_interactive_map.git
- revision: dd685166fdf953a11bd6a9e0dac56feca3bd0708
+ revision: 1ff222533cb3e7c30c8112a56c09c217c0530dbc
branch: release/0.27-stable
specs:
- decidim-homepage_interactive_map (2.0.0)
+ decidim-homepage_interactive_map (2.0.1)
decidim-admin (>= 0.25.0, < 0.28)
decidim-core (>= 0.25.0, < 0.28)
decidim-dev (>= 0.25.0, < 0.28)
From e4b8ae2eecc2eb7f7772914a55f3730b71748985 Mon Sep 17 00:00:00 2001
From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com>
Date: Fri, 22 Nov 2024 14:28:54 +0100
Subject: [PATCH 05/16] feat: Add module emitter (#633)
* feat: Addition of the emitter module
* fix: Fix failing specs
---
Gemfile | 1 +
Gemfile.lock | 9 +
.../participatory_processes/_form.html.erb | 234 ++++++++
config/i18n-tasks.yml | 1 +
config/initializers/half_signup.rb | 2 +
...m_participatory_process.decidim_emitter.rb | 14 +
db/schema.rb | 4 +-
spec/factories.rb | 1 +
spec/shared/manage_processes_examples.rb | 194 ++++++
..._administration_by_admin_shared_context.rb | 8 +
...y_process_administration_shared_context.rb | 6 +
...in_manages_participatory_processes_spec.rb | 560 ++++++++++++++++++
spec/system/participatory_processes_spec.rb | 70 +++
13 files changed, 1103 insertions(+), 1 deletion(-)
create mode 100644 app/views/decidim/participatory_processes/admin/participatory_processes/_form.html.erb
create mode 100644 db/migrate/20241118114335_add_emitter_to_decidim_participatory_process.decidim_emitter.rb
create mode 100644 spec/shared/manage_processes_examples.rb
create mode 100644 spec/shared/participatory_process_administration_by_admin_shared_context.rb
create mode 100644 spec/shared/participatory_process_administration_shared_context.rb
create mode 100644 spec/system/admin/admin_manages_participatory_processes_spec.rb
diff --git a/Gemfile b/Gemfile
index a33f8c0198..fbe02a049a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -26,6 +26,7 @@ gem "decidim-category_enhanced", "~> 0.0.1"
gem "decidim-cleaner"
gem "decidim-custom_proposal_states", git: "https://github.com/alecslupu-pfa/decidim-module-custom_proposal_states", branch: DECIDIM_BRANCH
gem "decidim-decidim_awesome", git: "https://github.com/decidim-ice/decidim-module-decidim_awesome", branch: DECIDIM_BRANCH
+gem "decidim-emitter", git: "https://github.com/OpenSourcePolitics/decidim-module-emitter.git"
gem "decidim-extended_socio_demographic_authorization_handler", git: "https://github.com/OpenSourcePolitics/decidim-module-extended_socio_demographic_authorization_handler.git",
branch: DECIDIM_BRANCH
gem "decidim-extra_user_fields", git: "https://github.com/OpenSourcePolitics/decidim-module-extra_user_fields.git", branch: "temp/twilio-compatibility-0.27"
diff --git a/Gemfile.lock b/Gemfile.lock
index 2364e94449..979cd98b7a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -7,6 +7,14 @@ GIT
decidim-core (>= 0.27.0)
deface (~> 1.5)
+GIT
+ remote: https://github.com/OpenSourcePolitics/decidim-module-emitter.git
+ revision: 8633ea56b422eecfe7d8730c89f191387f860e55
+ specs:
+ decidim-emitter (0.1.0)
+ decidim-core (~> 0.27.0)
+ decidim-participatory_processes (~> 0.27.0)
+
GIT
remote: https://github.com/OpenSourcePolitics/decidim-module-extended_socio_demographic_authorization_handler.git
revision: adec5e66cd07b5e5fdce5562453a7e8d6de88013
@@ -1179,6 +1187,7 @@ DEPENDENCIES
decidim-custom_proposal_states!
decidim-decidim_awesome!
decidim-dev (~> 0.27.0)
+ decidim-emitter!
decidim-extended_socio_demographic_authorization_handler!
decidim-extra_user_fields!
decidim-friendly_signup!
diff --git a/app/views/decidim/participatory_processes/admin/participatory_processes/_form.html.erb b/app/views/decidim/participatory_processes/admin/participatory_processes/_form.html.erb
new file mode 100644
index 0000000000..258a900133
--- /dev/null
+++ b/app/views/decidim/participatory_processes/admin/participatory_processes/_form.html.erb
@@ -0,0 +1,234 @@
+
+
+
<%= t(".title") %>
+
+
+
+
+ <%= form.translated :text_field, :title, autofocus: true %>
+
+
+
+ <%= form.translated :text_field, :subtitle %>
+
+
+
+ <%= form.number_field :weight %>
+
+
+
+
+ <%= form.text_field :slug %>
+
+ <%== t(".slug_help", url: decidim_form_slug_url(:processes, form.object.slug)) %>
+
+
+
+
+ <%= form.text_field :hashtag %>
+
+
+
+
+ <%= form.translated :editor, :short_description %>
+
+
+
+ <%= form.translated :editor, :description, toolbar: :full, lines: 25 %>
+
+
+
+ <%= form.translated :editor, :announcement %>
+
<%== t(".announcement_help") %>
+
+
+
+ <% if Decidim::Map.available?(:geocoding) %>
+
+ <%= form.text_field :address %>
+
<%== t(".address_help") %>
+
+ <% end %>
+
+
+
<%= t(".duration") %>
+
+
+
+
+
+ <%= form.date_field :start_date %>
+
+
+
+ <%= form.date_field :end_date %>
+
+
+
+
+
+
<%= t(".images") %>
+
+
+
+
+
+ <%= form.upload :hero_image %>
+
+
+
+ <%= form.upload :banner_image %>
+
+
+
+
+
+
<%= t(".metadata") %>
+
+
+
+
+
+ <%= form.translated :text_field, :developer_group %>
+
+
+
+ <%= form.translated :text_field, :local_area %>
+
+
+
+
+ <%= form.translated :text_field, :meta_scope %>
+
+
+
+ <%= form.translated :text_field, :target %>
+
+
+
+ <%= form.translated :text_field, :participatory_scope %>
+
+
+
+ <%= form.translated :text_field, :participatory_structure %>
+
+
+
+
+
<%= t(".filters") %>
+
+
+
+
+ <%= form.check_box :scopes_enabled %>
+
+
+
+ <%= scopes_picker_field form, :scope_id, root: nil %>
+
+
+ <%= form.collection_select :scope_type_max_depth_id,
+ organization_scope_depths,
+ :id,
+ :name,
+ scope_type_depth_select_options,
+ scope_type_depth_select_html_options %>
+
+ <%== t(".scope_type_max_depth_help") %>
+
+
+
+
+
+ <%= form.areas_select :area_id,
+ areas_for_select(current_organization),
+ selected: current_participatory_process.try(:decidim_area_id),
+ include_blank: t(".select_an_area") %>
+
+
+
+
+
<%= t(".visbility") %>
+
+
+
+
+ <% if process_groups_for_select %>
+ <%= form.select :participatory_process_group_id,
+ process_groups_for_select,
+ include_blank: t(".select_process_group") %>
+ <% end %>
+
+
+
+ <%= form.check_box :private_space %>
+
+
+ <%= form.check_box :promoted %>
+
+
+
+
+
<%= t(".emitter") %>
+
+
+
+
+ <%= form.select :emitter_select, options_for_select(emitter_options), { :include_blank => true, label: t(".emitter_logo_select") }, class: "select-emitter" %>
+
+
+
+
+ <%= form.text_field :emitter_name_image, label: t(".emitter_name") %>
+
+
+ <%= form.upload :emitter_image, label: t(".emitter_logo"), help_i18n_scope: "decidim.admin.forms.file_help.emitter" %>
+
+ <% if form.object.emitter_name.present? %>
+
+ <%= form.text_field :emitter_read_name, { :readonly => true, :label => t(".emitter_now") } %>
+
+ <% end %>
+
+
+
+
<%= t(".related_processes") %>
+
+
+
+
+ <%= form.select(
+ :related_process_ids,
+ @form.processes.order(title: :asc).map{|process| [translated_attribute(process.title), process.id]},
+ { include_blank: true },
+ { multiple: true, class: "chosen-select" }
+ ) %>
+
+
+
+
+
<%= t(".other") %>
+
+
+
+
+ <%= form.check_box :show_statistics %>
+
+
+
+ <%= form.check_box :show_metrics %>
+
+
+ <% if @form.participatory_process_types_for_select.present? %>
+
+ <%= form.select(
+ :participatory_process_type_id,
+ @form.participatory_process_types_for_select,
+ include_blank: t(".select_participatory_process_type")
+ ) %>
+
+ <% end %>
+
+
+
+<%= javascript_pack_tag "decidim_participatory_processes_admin" %>
\ No newline at end of file
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 7a81861781..db9522009e 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -119,6 +119,7 @@ ignore_missing:
- decidim.term_customizer.admin.actions.*
- decidim.term_customizer.admin.add_translations.index.*
- decidim.term_customizer.admin.models.translations.fields.*
+ - decidim.participatory_processes.admin.participatory_processes.form.*
# Consider these keys used:
ignore_unused:
diff --git a/config/initializers/half_signup.rb b/config/initializers/half_signup.rb
index 29512c6406..33b729485b 100644
--- a/config/initializers/half_signup.rb
+++ b/config/initializers/half_signup.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+return unless defined?(Decidim::HalfSignup)
+
Decidim::HalfSignup.configure do |config|
config.show_tos_page_after_signup = Rails.application.secrets.dig(:decidim, :half_signup, :show_tos_page_after_signup)
config.auth_code_length = 4
diff --git a/db/migrate/20241118114335_add_emitter_to_decidim_participatory_process.decidim_emitter.rb b/db/migrate/20241118114335_add_emitter_to_decidim_participatory_process.decidim_emitter.rb
new file mode 100644
index 0000000000..ba525aa3c4
--- /dev/null
+++ b/db/migrate/20241118114335_add_emitter_to_decidim_participatory_process.decidim_emitter.rb
@@ -0,0 +1,14 @@
+# This migration comes from decidim_emitter (originally 20240417082337)
+class AddEmitterToDecidimParticipatoryProcess < ActiveRecord::Migration[6.1]
+ def up
+ # Ensure that the column is a string and check if it exists
+ add_column :decidim_participatory_processes, :emitter, :string, if_not_exists: true
+ change_column :decidim_participatory_processes, :emitter, :string
+
+ add_column :decidim_participatory_processes, :emitter_name, :text, if_not_exists: true
+ end
+
+ def down
+ remove_column :decidim_participatory_processes, :emitter_name, if_exists: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b817eee201..4432bd4822 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2024_10_28_094242) do
+ActiveRecord::Schema.define(version: 2024_11_18_114335) do
# These are extensions that must be enabled in order to support this database
enable_extension "ltree"
@@ -1518,6 +1518,8 @@
t.float "longitude"
t.boolean "display_linked_assemblies", default: false
t.bigint "decidim_participatory_process_type_id"
+ t.string "emitter"
+ t.text "emitter_name"
t.index ["decidim_area_id"], name: "index_decidim_participatory_processes_on_decidim_area_id"
t.index ["decidim_organization_id", "slug"], name: "index_unique_process_slug_and_organization", unique: true
t.index ["decidim_organization_id"], name: "index_decidim_processes_on_decidim_organization_id"
diff --git a/spec/factories.rb b/spec/factories.rb
index febc0d8333..2c9271e1b1 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -4,6 +4,7 @@
require "decidim/proposals/test/factories"
require "decidim/budgets/test/factories"
require "decidim/debates/test/factories"
+require "decidim/emitter/test/factories"
require "decidim/meetings/test/factories"
require "decidim/accountability/test/factories"
require "decidim/system/test/factories"
diff --git a/spec/shared/manage_processes_examples.rb b/spec/shared/manage_processes_examples.rb
new file mode 100644
index 0000000000..b72c16f0a4
--- /dev/null
+++ b/spec/shared/manage_processes_examples.rb
@@ -0,0 +1,194 @@
+# frozen_string_literal: true
+
+shared_examples "manage processes examples" do
+ context "when viewing the processes list" do
+ let!(:process_group) { create(:participatory_process_group, organization: organization) }
+ let!(:process_with_group) { create(:participatory_process, organization: organization, participatory_process_group: process_group) }
+ let!(:process_without_group) { create(:participatory_process, organization: organization) }
+ let(:model_name) { participatory_process.class.model_name }
+ let(:resource_controller) { Decidim::ParticipatoryProcesses::Admin::ParticipatoryProcessesController }
+
+ def filter_by_group(group_title)
+ visit current_path
+ within(".card-title") do
+ click_button("Process Groups")
+ click_link(group_title)
+ end
+ end
+
+ it "allows the user to filter processes by process_group" do
+ filter_by_group(translated(process_group.title))
+
+ expect(page).to have_content(translated(process_with_group.title))
+ expect(page).not_to have_content(translated(process_without_group.title))
+ end
+
+ describe "listing processes" do
+ it_behaves_like "filtering collection by published/unpublished"
+ it_behaves_like "filtering collection by private/public"
+ end
+
+ context "when processes are filtered by process_group" do
+ before { filter_by_group(translated(process_group.title)) }
+
+ it "allows the user to edit the process_group" do
+ click_link translated(process_group.title)
+
+ expect(page).to have_content("Edit process group")
+ end
+
+ describe "listing processes filtered by group" do
+ it_behaves_like "filtering collection by published/unpublished" do
+ let!(:published_space) { process_with_group }
+ let!(:unpublished_space) { create(:participatory_process, :unpublished, organization: organization, participatory_process_group: process_group) }
+ end
+
+ it_behaves_like "filtering collection by private/public" do
+ let!(:public_space) { process_with_group }
+ let!(:private_space) { create(:participatory_process, :private, organization: organization, participatory_process_group: process_group) }
+ end
+ end
+ end
+ end
+
+ context "when previewing processes" do
+ context "when the process is unpublished" do
+ let!(:participatory_process) { create(:participatory_process, :unpublished, organization: organization) }
+
+ it "allows the user to preview the unpublished process" do
+ within find("tr", text: translated(participatory_process.title)) do
+ click_link "Preview"
+ end
+
+ expect(page).to have_css(".process-header")
+ expect(page).to have_content(translated(participatory_process.title))
+ end
+ end
+
+ context "when the process is published" do
+ let!(:participatory_process) { create(:participatory_process, organization: organization) }
+
+ it "allows the user to preview the published process" do
+ within find("tr", text: translated(participatory_process.title)) do
+ click_link "Preview"
+ end
+
+ expect(page).to have_current_path decidim_participatory_processes.participatory_process_path(participatory_process)
+ expect(page).to have_content(translated(participatory_process.title))
+ end
+ end
+ end
+
+ context "when viewing a missing process" do
+ it_behaves_like "a 404 page" do
+ let(:target_path) { decidim_admin_participatory_processes.participatory_process_path(99_999_999) }
+ end
+ end
+
+ context "when updating a participatory process" do
+ let(:image3_filename) { "city3.jpeg" }
+ let(:image3_path) { Decidim::Dev.asset(image3_filename) }
+
+ before do
+ click_link translated(participatory_process.title)
+ end
+
+ it "updates a participatory_process" do
+ fill_in_i18n(
+ :participatory_process_title,
+ "#participatory_process-title-tabs",
+ en: "My new title",
+ es: "Mi nuevo título",
+ ca: "El meu nou títol"
+ )
+ dynamically_attach_file(:participatory_process_banner_image, image3_path, remove_before: true)
+
+ page.execute_script("$('#participatory_process_end_date').focus()")
+ page.find(".datepicker-dropdown .day", text: "22").click
+
+ within ".edit_participatory_process" do
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_admin_callout("successfully")
+
+ within ".container" do
+ expect(page).to have_selector("input[value='My new title']")
+ expect(page).to have_css("img[src*='#{image3_filename}']")
+ end
+ end
+ end
+
+ context "when publishing a process" do
+ let!(:participatory_process) { create(:participatory_process, :unpublished, organization: organization) }
+
+ before do
+ click_link translated(participatory_process.title)
+ end
+
+ it "publishes the process" do
+ click_link "Publish"
+ expect(page).to have_content("successfully published")
+ expect(page).to have_content("Unpublish")
+ expect(page).to have_current_path decidim_admin_participatory_processes.edit_participatory_process_path(participatory_process)
+
+ participatory_process.reload
+ expect(participatory_process).to be_published
+ end
+ end
+
+ context "when unpublishing a process" do
+ let!(:participatory_process) { create(:participatory_process, organization: organization) }
+
+ before do
+ click_link translated(participatory_process.title)
+ end
+
+ it "unpublishes the process" do
+ click_link "Unpublish"
+ expect(page).to have_content("successfully unpublished")
+ expect(page).to have_content("Publish")
+ expect(page).to have_current_path decidim_admin_participatory_processes.edit_participatory_process_path(participatory_process)
+
+ participatory_process.reload
+ expect(participatory_process).not_to be_published
+ end
+ end
+
+ context "when there are multiple organizations in the system" do
+ let!(:external_participatory_process) { create(:participatory_process) }
+
+ before do
+ visit decidim_admin_participatory_processes.participatory_processes_path
+ end
+
+ it "doesn't let the admin manage processes form other organizations" do
+ within "table" do
+ expect(page).to have_no_content(external_participatory_process.title["en"])
+ end
+ end
+ end
+
+ context "when the process has a scope" do
+ let(:scope) { create(:scope, organization: organization) }
+
+ before do
+ participatory_process.update!(scopes_enabled: true, scope: scope)
+ end
+
+ it "disables the scope for a participatory process" do
+ click_link translated(participatory_process.title)
+
+ uncheck :participatory_process_scopes_enabled
+
+ expect(page).to have_selector("#participatory_process_scope_id.disabled")
+ expect(page).to have_selector("#participatory_process_scope_id .picker-values div input[disabled]", visible: :all)
+
+ within ".edit_participatory_process" do
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_admin_callout("successfully")
+ end
+ end
+end
diff --git a/spec/shared/participatory_process_administration_by_admin_shared_context.rb b/spec/shared/participatory_process_administration_by_admin_shared_context.rb
new file mode 100644
index 0000000000..014fd52a16
--- /dev/null
+++ b/spec/shared/participatory_process_administration_by_admin_shared_context.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+shared_context "when admin administrating a participatory process" do
+ let!(:user) do
+ create(:user, :admin, :confirmed, organization: organization)
+ end
+ include_context "when administrating a participatory process"
+end
diff --git a/spec/shared/participatory_process_administration_shared_context.rb b/spec/shared/participatory_process_administration_shared_context.rb
new file mode 100644
index 0000000000..ff0cc89db9
--- /dev/null
+++ b/spec/shared/participatory_process_administration_shared_context.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+shared_context "when administrating a participatory process" do
+ let(:organization) { create(:organization) }
+ let!(:participatory_process) { create(:participatory_process, organization: organization) }
+end
diff --git a/spec/system/admin/admin_manages_participatory_processes_spec.rb b/spec/system/admin/admin_manages_participatory_processes_spec.rb
new file mode 100644
index 0000000000..ec97e30dbb
--- /dev/null
+++ b/spec/system/admin/admin_manages_participatory_processes_spec.rb
@@ -0,0 +1,560 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+require "decidim/core/test/shared_examples/has_contextual_help"
+
+describe "Participatory Processes", type: :system do
+ let(:organization) { create(:organization) }
+ let(:show_metrics) { true }
+ let(:show_statistics) { true }
+ let(:hashtag) { true }
+ let(:base_description) { { en: "Description", ca: "Descripció", es: "Descripción" } }
+ let(:short_description) { { en: "Short description", ca: "Descripció curta", es: "Descripción corta" } }
+ let(:base_process) do
+ create(
+ :participatory_process,
+ :active,
+ organization: organization,
+ description: base_description,
+ short_description: short_description,
+ show_metrics: show_metrics,
+ show_statistics: show_statistics
+ )
+ end
+
+ before do
+ switch_to_host(organization.host)
+ end
+
+ context "when there are no processes and directly accessing form URL" do
+ it_behaves_like "a 404 page" do
+ let(:target_path) { decidim_participatory_processes.participatory_processes_path }
+ end
+ end
+
+ context "when there are no processes and accessing from the homepage" do
+ it "does not show the menu link" do
+ visit decidim.root_path
+
+ within ".main-nav" do
+ expect(page).to have_no_content("Processes")
+ end
+ end
+ end
+
+ context "when the process does not exist" do
+ it_behaves_like "a 404 page" do
+ let(:target_path) { decidim_participatory_processes.participatory_process_path(99_999_999) }
+ end
+ end
+
+ context "when there are some processes and all are unpublished" do
+ before do
+ create(:participatory_process, :unpublished, organization: organization)
+ create(:participatory_process, :published)
+ end
+
+ context "and directly accessing from URL" do
+ it_behaves_like "a 404 page" do
+ let(:target_path) { decidim_participatory_processes.participatory_processes_path }
+ end
+ end
+
+ context "and accessing from the homepage" do
+ it "the menu link is not shown" do
+ visit decidim.root_path
+
+ within ".main-nav" do
+ expect(page).to have_no_content("Processes")
+ end
+ end
+ end
+ end
+
+ context "when there are some published processes" do
+ let!(:participatory_process) { base_process }
+ let!(:promoted_process) { create(:participatory_process, :promoted, organization: organization) }
+ let!(:unpublished_process) { create(:participatory_process, :unpublished, organization: organization) }
+ let!(:past_process) { create :participatory_process, :past, organization: organization }
+ let!(:upcoming_process) { create :participatory_process, :upcoming, organization: organization }
+ let!(:grouped_process) { create :participatory_process, organization: organization }
+ let!(:group) { create :participatory_process_group, participatory_processes: [grouped_process], organization: organization }
+
+ it_behaves_like "shows contextual help" do
+ let(:index_path) { decidim_participatory_processes.participatory_processes_path }
+ let(:manifest_name) { :participatory_processes }
+ end
+
+ it_behaves_like "editable content for admins" do
+ let(:target_path) { decidim_participatory_processes.participatory_processes_path }
+ end
+
+ context "when requesting the processes path" do
+ before do
+ visit decidim_participatory_processes.participatory_processes_path
+ end
+
+ it_behaves_like "accessible page"
+
+ context "when emitter is defined", :slow do
+ context "when no emitter" do
+ it "doesn't displays logo or text" do
+ within "#participatory_process_#{promoted_process.id}" do
+ expect(page).not_to have_css(".emitter-header")
+ end
+ end
+ end
+
+ context "when emitter" do
+ let(:base_process) do
+ create(
+ :participatory_process,
+ :active,
+ :with_emitter,
+ organization: organization,
+ description: { en: "Description", ca: "Descripció", es: "Descripción" },
+ short_description: { en: "Short description", ca: "Descripció curta", es: "Descripción corta" },
+ show_metrics: show_metrics,
+ show_statistics: show_statistics,
+ developer_group: { en: "Developer group" }
+ )
+ end
+
+ it "displays logo and text" do
+ within "#participatory_process_#{base_process.id}" do
+ within ".emitter-header" do
+ expect(page).to have_css("img", count: 1)
+ expect(page).to have_content("Consultation published by Developer group")
+ end
+ end
+ end
+ end
+ end
+
+ context "and accessing from the homepage" do
+ it "the menu link is not shown" do
+ visit decidim.root_path
+
+ within ".main-nav" do
+ expect(page).to have_content("Processes")
+ click_link "Processes"
+ end
+
+ expect(page).to have_current_path decidim_participatory_processes.participatory_processes_path
+ end
+ end
+
+ context "with highlighted processes" do
+ before do
+ promoted_process.title["en"] = "D'Artagnan #{promoted_process.title["en"]}"
+ promoted_process.save!
+ visit decidim_participatory_processes.participatory_processes_path
+ end
+
+ it_behaves_like "accessible page"
+
+ context "when emitter is defined", :slow do
+ context "when no emitter" do
+ it "doesn't displays logo or text" do
+ within "#participatory_process_#{promoted_process.id}" do
+ expect(page).not_to have_css(".emitter-header")
+ end
+ end
+ end
+
+ context "when emitter" do
+ let(:base_process) do
+ create(
+ :participatory_process,
+ :active,
+ :with_emitter,
+ organization: organization,
+ description: { en: "Description", ca: "Descripció", es: "Descripción" },
+ short_description: { en: "Short description", ca: "Descripció curta", es: "Descripción corta" },
+ show_metrics: show_metrics,
+ show_statistics: show_statistics,
+ developer_group: { en: "Developer group" }
+ )
+ end
+
+ it "displays logo and text" do
+ within "#participatory_process_#{base_process.id}" do
+ within ".emitter-header" do
+ expect(page).to have_css("img", count: 1)
+ expect(page).to have_content("Consultation published by Developer group")
+ end
+ end
+ end
+ end
+
+ it "lists all the highlighted processes" do
+ within "#highlighted-processes" do
+ expect(page).to have_content(translated(promoted_process.title, locale: :en))
+ expect(page).to have_selector(".card--full", count: 1)
+ end
+ end
+ end
+
+ it "lists the active processes" do
+ within "#processes-grid" do
+ within "#processes-grid h3" do
+ expect(page).to have_content("3 ACTIVE PROCESSES")
+ end
+
+ expect(page).to have_content(translated(participatory_process.title, locale: :en))
+ expect(page).to have_content(translated(promoted_process.title, locale: :en))
+ expect(page).to have_content(translated(group.title, locale: :en))
+ expect(page).to have_selector(".card", count: 3)
+
+ expect(page).to have_no_content(translated(unpublished_process.title, locale: :en))
+ expect(page).to have_no_content(translated(past_process.title, locale: :en))
+ expect(page).to have_no_content(translated(upcoming_process.title, locale: :en))
+ expect(page).to have_no_content(translated(grouped_process.title, locale: :en))
+ end
+ end
+
+ it "links to the individual process page" do
+ first(".card__link", text: translated(participatory_process.title, locale: :en)).click
+
+ expect(page).to have_current_path decidim_participatory_processes.participatory_process_path(participatory_process)
+ end
+
+ context "with active steps" do
+ let!(:step) { create(:participatory_process_step, participatory_process: participatory_process) }
+ let!(:active_step) do
+ create(:participatory_process_step,
+ :active,
+ participatory_process: participatory_process,
+ title: { en: "Active step", ca: "Fase activa", es: "Fase activa" })
+ end
+
+ it "links to the active step" do
+ visit decidim_participatory_processes.participatory_processes_path
+
+ within find("#processes-grid .column", text: translated(participatory_process.title)) do
+ within ".card__footer" do
+ expect(page).to have_content("Current phase:\nActive step")
+ end
+ end
+ end
+
+ context "when the active step has CTA text and url set" do
+ let(:cta_path) { "my_path" }
+ let(:cta_text) { { en: "Take action!", ca: "Take action!", es: "Take action!" } }
+
+ before do
+ active_step.update!(cta_path: cta_path, cta_text: cta_text)
+ end
+
+ it "shows a CTA button" do
+ visit decidim_participatory_processes.participatory_processes_path
+
+ within "#participatory_process_#{participatory_process.id}" do
+ expect(page).to have_link("Take action!")
+ end
+ end
+
+ context "when cta_text is empty in current locale" do
+ let(:cta_text) { { en: "", ca: "Take action!", es: "Take action!" } }
+
+ it "displays the regular cta button" do
+ visit decidim_participatory_processes.participatory_processes_path
+
+ within "#participatory_process_#{participatory_process.id}" do
+ expect(page).not_to have_link("Take action!")
+ expect(page).to have_link("More info")
+ end
+ end
+ end
+
+ context "when process is promoted" do
+ let(:cta_text) { { en: "Take promoted action!", ca: "Take promoted action!", es: "Take promoted action!" } }
+ let!(:active_step) do
+ create(:participatory_process_step,
+ :active,
+ participatory_process: promoted_process,
+ title: { en: "Active step", ca: "Fase activa", es: "Fase activa" })
+ end
+
+ it "shows a CTA button" do
+ visit decidim_participatory_processes.participatory_processes_path
+
+ within "#highlighted-processes" do
+ expect(page).to have_link("Take promoted action!")
+ end
+ end
+ end
+
+ context "when user switch locale" do
+ before do
+ visit decidim_participatory_processes.participatory_processes_path
+ within_language_menu do
+ click_link "Català"
+ end
+ end
+
+ it "displays the regular cta button" do
+ within "#participatory_process_#{participatory_process.id}" do
+ expect(page).to have_link("Take action!", href: "/processes/#{participatory_process.slug}/my_path")
+ end
+ end
+ end
+ end
+ end
+
+ context "when there are promoted participatory process groups" do
+ let!(:promoted_group) { create(:participatory_process_group, :promoted, :with_participatory_processes, organization: organization) }
+ let(:promoted_items_titles) { page.all("#highlighted-processes .card__title").map(&:text) }
+
+ before do
+ promoted_group.title["en"] = "D'Artagnan #{promoted_group.title["en"]}"
+ promoted_group.save!
+ visit decidim_participatory_processes.participatory_processes_path
+ end
+
+ it "shows a highligted processes section" do
+ expect(page).to have_content("HIGHLIGHTED PROCESSES")
+ end
+
+ it "lists only promoted groups" do
+ expect(promoted_items_titles).to include(translated(promoted_group.title, locale: :en))
+ expect(promoted_items_titles).not_to include(translated(group.title, locale: :en))
+ end
+
+ it "lists all the highlighted process groups" do
+ within "#highlighted-processes" do
+ expect(page).to have_content(translated(promoted_group.title, locale: :en))
+ expect(page).to have_selector(".card--full", count: 2)
+ end
+ end
+
+ context "and promoted group has defined a CTA content block" do
+ let(:cta_settings) do
+ {
+ button_url: "https://example.org/action",
+ button_text_en: "cta text",
+ description_en: "cta description"
+ }
+ end
+
+ before do
+ create(
+ :content_block,
+ organization: organization,
+ scope_name: :participatory_process_group_homepage,
+ scoped_resource_id: promoted_group.id,
+ manifest_name: :cta,
+ settings: cta_settings
+ )
+ visit decidim_participatory_processes.participatory_processes_path
+ end
+
+ it "shows a CTA button inside group card" do
+ within("#highlighted-processes") do
+ expect(page).to have_link(cta_settings[:button_text_en], href: cta_settings[:button_url])
+ end
+ end
+
+ context "and promoted group belongs to another organization" do
+ let!(:promoted_group) { create(:participatory_process_group, :promoted, :with_participatory_processes) }
+
+ it "shows a CTA button inside group card" do
+ within("#highlighted-processes") do
+ expect(page).not_to have_link(cta_settings[:button_text_en], href: cta_settings[:button_url])
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ context "when going to the participatory process page" do
+ let!(:participatory_process) { base_process }
+ let!(:proposals_component) { create(:extended_proposal_component, :published, participatory_space: participatory_process, manifest_name: :proposals) }
+ let!(:meetings_component) { create(:component, :unpublished, participatory_space: participatory_process, manifest_name: :meetings) }
+
+ before do
+ create_list(:extended_proposal, 3, component: proposals_component)
+ allow(Decidim).to receive(:component_manifests).and_return([proposals_component.manifest, meetings_component.manifest])
+ end
+
+ it_behaves_like "editable content for admins" do
+ let(:target_path) { decidim_participatory_processes.participatory_process_path(participatory_process) }
+ end
+
+ context "when requesting the participatory process path" do
+ before do
+ visit decidim_participatory_processes.participatory_process_path(participatory_process)
+ end
+
+ context "when requesting the process path" do
+ it "shows the details of the given process" do
+ within "main" do
+ expect(page).to have_content(translated(participatory_process.title, locale: :en))
+ expect(page).to have_content(translated(participatory_process.subtitle, locale: :en))
+ expect(page).to have_content(translated(participatory_process.description, locale: :en))
+ expect(page).to have_content(translated(participatory_process.short_description, locale: :en))
+ expect(page).to have_content(translated(participatory_process.meta_scope, locale: :en))
+ expect(page).to have_content(translated(participatory_process.developer_group, locale: :en))
+ expect(page).to have_content(translated(participatory_process.local_area, locale: :en))
+ expect(page).to have_content(translated(participatory_process.target, locale: :en))
+ expect(page).to have_content(translated(participatory_process.participatory_scope, locale: :en))
+ expect(page).to have_content(translated(participatory_process.participatory_structure, locale: :en))
+ expect(page).to have_content(I18n.l(participatory_process.end_date, format: :long))
+ expect(page).to have_content(participatory_process.hashtag)
+ end
+ end
+
+ it_behaves_like "has attachments" do
+ let(:attached_to) { participatory_process }
+ end
+
+ it_behaves_like "has attachment collections" do
+ let(:attached_to) { participatory_process }
+ let(:collection_for) { participatory_process }
+ end
+
+ context "and it belongs to a group" do
+ let!(:group) { create :participatory_process_group, participatory_processes: [participatory_process], organization: organization }
+
+ it "has a link to the group the process belongs to" do
+ visit decidim_participatory_processes.participatory_process_path(participatory_process)
+
+ expect(page).to have_link(translated(group.title, locale: :en), href: decidim_participatory_processes.participatory_process_group_path(group))
+ end
+ end
+
+ context "when it has some linked processes" do
+ let(:published_process) { create :participatory_process, :published, organization: organization }
+ let(:unpublished_process) { create :participatory_process, :unpublished, organization: organization }
+
+ it "only shows the published linked processes" do
+ participatory_process
+ .link_participatory_space_resources(
+ [published_process, unpublished_process],
+ "related_processes"
+ )
+ visit decidim_participatory_processes.participatory_process_path(participatory_process)
+ expect(page).to have_content(translated(published_process.title))
+ expect(page).to have_no_content(translated(unpublished_process.title))
+ end
+ end
+
+ context "and the process has some components" do
+ it "shows the components" do
+ within ".process-nav" do
+ expect(page).to have_content(translated(proposals_component.name, locale: :en).upcase)
+ expect(page).to have_no_content(translated(meetings_component.name, locale: :en).upcase)
+ end
+ end
+
+ context "and the process metrics are enabled" do
+ let(:organization) { create(:organization) }
+ let(:metrics) do
+ Decidim.metrics_registry.filtered(highlight: true, scope: "participatory_process").each do |metric_registry|
+ create(:metric, metric_type: metric_registry.metric_name, day: Time.zone.today - 1.week, organization: organization, participatory_space_type: Decidim::ParticipatoryProcess.name, participatory_space_id: participatory_process.id, cumulative: 5, quantity: 2)
+ end
+ end
+
+ before do
+ metrics
+ visit current_path
+ end
+
+ it "shows the metrics charts" do
+ expect(page).to have_css("h3.section-heading", text: "METRICS")
+
+ within "#metrics" do
+ expect(page).to have_css("input#metrics-space_type[value='Decidim::ParticipatoryProcess']", visible: :hidden)
+ expect(page).to have_css("input#metrics-space_id[value='#{participatory_process.id}']", visible: :hidden)
+ Decidim.metrics_registry.filtered(highlight: true, scope: "participatory_process").each do |metric_registry|
+ expect(page).to have_css(%(##{metric_registry.metric_name}_chart))
+ end
+ end
+ end
+
+ it "renders a link to all metrics" do
+ within "#metrics" do
+ expect(page).to have_link("Show all metrics")
+ end
+ end
+
+ it "click link" do
+ click_link("Show all metrics")
+ have_current_path(decidim_participatory_processes.all_metrics_participatory_process_path(participatory_process))
+ end
+ end
+
+ context "and the process statistics are enabled" do
+ let(:show_statistics) { true }
+
+ it "the stats for those components are visible" do
+ within ".section-statistics" do
+ expect(page).to have_css("h3.section-heading", text: "STATISTICS")
+ expect(page).to have_css(".statistic__title", text: "PROPOSALS")
+ expect(page).to have_css(".statistic__number", text: "3")
+ expect(page).to have_no_css(".statistic__title", text: "MEETINGS")
+ expect(page).to have_no_css(".statistic__number", text: "0")
+ end
+ end
+ end
+
+ context "and the process statistics are not enabled" do
+ let(:show_statistics) { false }
+
+ it "the stats for those components are not visible" do
+ expect(page).to have_no_css("h3.section-heading", text: "STATISTICS")
+ expect(page).to have_no_css(".statistic__title", text: "PROPOSALS")
+ expect(page).to have_no_css(".statistic__number", text: "3")
+ end
+ end
+
+ context "and the process metrics are not enabled" do
+ let(:show_metrics) { false }
+
+ it "the metrics for the participatory processes are not rendered" do
+ expect(page).to have_no_css("h4.section-heading", text: "METRICS")
+ end
+
+ it "has no link to all metrics" do
+ expect(page).to have_no_link("Show all metrics")
+ end
+ end
+
+ context "and the process doesn't have hashtag" do
+ let(:hashtag) { false }
+
+ it "the hashtags for those components are not visible" do
+ expect(page).to have_no_content("#")
+ end
+ end
+ end
+
+ context "when assemblies are linked to participatory process" do
+ let!(:published_assembly) { create(:assembly, :published, organization: organization) }
+ let!(:unpublished_assembly) { create(:assembly, :unpublished, organization: organization) }
+ let!(:private_assembly) { create(:assembly, :published, :private, :opaque, organization: organization) }
+ let!(:transparent_assembly) { create(:assembly, :published, :private, :transparent, organization: organization) }
+
+ before do
+ published_assembly.link_participatory_space_resources(participatory_process, "included_participatory_processes")
+ unpublished_assembly.link_participatory_space_resources(participatory_process, "included_participatory_processes")
+ private_assembly.link_participatory_space_resources(participatory_process, "included_participatory_processes")
+ transparent_assembly.link_participatory_space_resources(participatory_process, "included_participatory_processes")
+ visit decidim_participatory_processes.participatory_process_path(participatory_process)
+ end
+
+ it "display related assemblies" do
+ expect(page).to have_content("RELATED ASSEMBLIES")
+ expect(page).to have_content(translated(published_assembly.title))
+ expect(page).to have_content(translated(transparent_assembly.title))
+ expect(page).to have_no_content(translated(unpublished_assembly.title))
+ expect(page).to have_no_content(translated(private_assembly.title))
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/system/participatory_processes_spec.rb b/spec/system/participatory_processes_spec.rb
index cacfdff136..f635bb873b 100644
--- a/spec/system/participatory_processes_spec.rb
+++ b/spec/system/participatory_processes_spec.rb
@@ -94,6 +94,41 @@
it_behaves_like "accessible page"
+ context "when emitter is defined", :slow do
+ context "when no emitter" do
+ it "doesn't displays logo or text" do
+ within "#participatory_process_#{promoted_process.id}" do
+ expect(page).not_to have_css(".emitter-header")
+ end
+ end
+ end
+
+ context "when emitter" do
+ let(:base_process) do
+ create(
+ :participatory_process,
+ :active,
+ :with_emitter,
+ organization: organization,
+ description: { en: "Description", ca: "Descripció", es: "Descripción" },
+ short_description: { en: "Short description", ca: "Descripció curta", es: "Descripción corta" },
+ show_metrics: show_metrics,
+ show_statistics: show_statistics,
+ developer_group: { en: "Developer group" }
+ )
+ end
+
+ it "displays logo and text" do
+ within "#participatory_process_#{base_process.id}" do
+ within ".emitter-header" do
+ expect(page).to have_css("img", count: 1)
+ expect(page).to have_content("Consultation published by Developer group")
+ end
+ end
+ end
+ end
+ end
+
context "and accessing from the homepage" do
it "the menu link is not shown" do
visit decidim.root_path
@@ -116,6 +151,41 @@
it_behaves_like "accessible page"
+ context "when emitter is defined", :slow do
+ context "when no emitter" do
+ it "doesn't displays logo or text" do
+ within "#participatory_process_#{promoted_process.id}" do
+ expect(page).not_to have_css(".emitter-header")
+ end
+ end
+ end
+
+ context "when emitter" do
+ let(:base_process) do
+ create(
+ :participatory_process,
+ :active,
+ :with_emitter,
+ organization: organization,
+ description: { en: "Description", ca: "Descripció", es: "Descripción" },
+ short_description: { en: "Short description", ca: "Descripció curta", es: "Descripción corta" },
+ show_metrics: show_metrics,
+ show_statistics: show_statistics,
+ developer_group: { en: "Developer group" }
+ )
+ end
+
+ it "displays logo and text" do
+ within "#participatory_process_#{base_process.id}" do
+ within ".emitter-header" do
+ expect(page).to have_css("img", count: 1)
+ expect(page).to have_content("Consultation published by Developer group")
+ end
+ end
+ end
+ end
+ end
+
it "lists all the highlighted processes" do
within "#highlighted-processes" do
expect(page).to have_content(translated(promoted_process.title, locale: :en))
From 933a234e430d5b5aab96061737c5e986db2a348d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E2=80=99Barbara=20Oliveira?=
<143180473+BarbaraOliveira13@users.noreply.github.com>
Date: Fri, 22 Nov 2024 15:01:08 +0100
Subject: [PATCH 06/16] Fix/backport decidim awesome slowness on proposals
index page (#631)
* add env variable
* add UUID and IP to logs
* add weighted voting configuration
* add secrets for weighted voting
* update test to fix CI
* continue fix test file
* fix CI
* fix CI
* clean spec
* clean and add test
---
.env-example | 4 +-
config/environments/development.rb | 1 +
config/initializers/decidim_awesome.rb | 5 +++
config/secrets.yml | 2 +
.../proposal_extra_field_spec.rb | 43 +++----------------
5 files changed, 16 insertions(+), 39 deletions(-)
create mode 100644 config/initializers/decidim_awesome.rb
diff --git a/.env-example b/.env-example
index 14c215885a..18d2e784f8 100644
--- a/.env-example
+++ b/.env-example
@@ -77,7 +77,9 @@ DECIDIM_ADMIN_PASSWORD_STRONG="false"
## Generate values with: bin/rails decidim:pwa:generate_vapid_keys
# VAPID_PUBLIC_KEY
# VAPID_PRIVATE_KEY
-RAILS_LOG_LEVEL=warn
+# RAILS_LOG_LEVEL=warn
+
+# DECIDIM_AWESOME_WEIGHTED_PROPOSAL_VOTING_ENABLED=disabled # or enabled
# Default notifications sending frequency : (daily, weekly, none, real_time)
# NOTIFICATIONS_SENDING_FREQUENCY=daily
diff --git a/config/environments/development.rb b/config/environments/development.rb
index d667dc6158..6ed8a74c31 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -60,4 +60,5 @@
# Setting this to 100 years should be enough
config.global_id.expires_in = 100.years
config.deface.enabled = ENV.fetch("DEFACE_ENABLED", nil) == "true"
+ config.log_tags = [:uuid, :remote_ip]
end
diff --git a/config/initializers/decidim_awesome.rb b/config/initializers/decidim_awesome.rb
new file mode 100644
index 0000000000..24a30cdbc2
--- /dev/null
+++ b/config/initializers/decidim_awesome.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+Decidim::DecidimAwesome.configure do |config|
+ config.weighted_proposal_voting = Rails.application.secrets.dig(:decidim, :decidim_awesome, :weighted_proposal_voting_enabled)&.to_sym
+end
diff --git a/config/secrets.yml b/config/secrets.yml
index 2f166fdfd3..78f94c3731 100644
--- a/config/secrets.yml
+++ b/config/secrets.yml
@@ -13,6 +13,8 @@
default: &default
asset_host: <%= ENV["ASSET_HOST"] %>
decidim:
+ decidim_awesome:
+ weighted_proposal_voting_enabled: <%= ENV.fetch("DECIDIM_AWESOME_WEIGHTED_PROPOSAL_VOTING_ENABLED", "disabled") %>
admin_password:
expiration_days: <%= ENV.fetch("DECIDIM_ADMIN_PASSWORD_EXPIRATION_DAYS", 365).to_i %>
min_length: <%= ENV.fetch("DECIDIM_ADMIN_PASSWORD_MIN_LENGTH", 15).to_i %>
diff --git a/spec/models/decidim/decidim_awesome/proposal_extra_field_spec.rb b/spec/models/decidim/decidim_awesome/proposal_extra_field_spec.rb
index 1223979e07..b903553fbd 100644
--- a/spec/models/decidim/decidim_awesome/proposal_extra_field_spec.rb
+++ b/spec/models/decidim/decidim_awesome/proposal_extra_field_spec.rb
@@ -8,6 +8,7 @@ module Decidim::DecidimAwesome
let(:extra_fields) { create(:awesome_proposal_extra_fields, proposal: create(:extended_proposal)) }
let(:proposal) { create(:extended_proposal) }
+ let(:component) { create(:component, settings: { awesome_voting_manifest: "default" }) }
it { is_expected.to be_valid }
@@ -183,44 +184,10 @@ module Decidim::DecidimAwesome
end
end
- describe "all_vote_weights" do
- let!(:extra_fields) { create(:awesome_proposal_extra_fields, proposal: proposal) }
- let!(:another_extra_fields) { create(:awesome_proposal_extra_fields, proposal: another_proposal) }
- let!(:unrelated_another_extra_fields) { create(:awesome_proposal_extra_fields, :with_votes, proposal: create(:extended_proposal)) }
- let(:another_proposal) { create(:proposal, component: proposal.component) }
- let!(:votes) do
- vote = create(:proposal_vote, proposal: proposal, author: create(:user, organization: proposal.organization))
- create(:awesome_vote_weight, vote: vote, weight: 1)
- end
- let!(:other_votes) do
- vote = create(:proposal_vote, proposal: another_proposal, author: create(:user, organization: proposal.organization))
- create(:awesome_vote_weight, vote: vote, weight: 2)
- end
-
- it "returns all vote weights for a component" do
- expect(proposal.reload.all_vote_weights).to contain_exactly(1, 2)
- expect(another_proposal.reload.all_vote_weights).to contain_exactly(1, 2)
- expect(proposal.vote_weights).to eq({ "1" => 1, "2" => 0 })
- expect(another_proposal.vote_weights).to eq({ "1" => 0, "2" => 1 })
- end
-
- context "when wrong cache exists" do
- before do
- # rubocop:disable Rails/SkipsModelValidations:
- # we don't want to trigger the active record hooks
- extra_fields.update_columns(vote_weight_totals: { "3" => 1, "4" => 1 })
- # rubocop:enable Rails/SkipsModelValidations:
- end
-
- it "returns all vote weights for a component" do
- expect(proposal.reload.extra_fields.vote_weight_totals).to eq({ "3" => 1, "4" => 1 })
- expect(proposal.vote_weights).to eq({ "1" => 0, "2" => 0 })
- proposal.update_vote_weights!
- expect(proposal.vote_weights).to eq({ "1" => 1, "2" => 0 })
- expect(another_proposal.reload.vote_weights).to eq({ "1" => 0, "2" => 1 })
- expect(proposal.extra_fields.vote_weight_totals).to eq({ "1" => 1 })
- expect(another_proposal.extra_fields.vote_weight_totals).to eq({ "2" => 1 })
- end
+ describe "weighted_proposal_voting_enabled" do
+ it "is disabled by default" do
+ default_value = Rails.application.secrets.dig(:decidim, :decidim_awesome, :weighted_proposal_voting_enabled)
+ expect(default_value).to eq("disabled")
end
end
From 1010895ea4ce2b563a0a79b7d95404f0f09e5d15 Mon Sep 17 00:00:00 2001
From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com>
Date: Fri, 22 Nov 2024 16:13:05 +0100
Subject: [PATCH 07/16] backport: Addition of sortable scopes using drag and
drop (#632)
* backport: Backport the Drag & Drop on Scopes on BackOffice
* fix: Fix locales that were not normalized or misplaced
* test: Add some specs about the check_boxes_tree_helper sort
* test: Add specs releated to the backport scopes management
* fix: Fix wrong locales
* fix: Add the weight sorting on select input of scopes
* fix: Add missing locale
---
app/commands/admin/reorder_scopes.rb | 37 ++++++
app/forms/decidim/user_interest_scope_form.rb | 25 ++++
app/forms/decidim/user_interests_form.rb | 23 ++++
.../simple_proposal/scopes_helper_override.rb | 48 +++++++
app/packs/entrypoints/application.js | 2 +
.../entrypoints/decidim_custom_scopes.scss | 1 +
app/packs/src/decidim/admin/reorder_scopes.js | 19 +++
.../decidim/scopes/_scopes-custom.scss | 18 +++
app/views/decidim/admin/scopes/index.html.erb | 65 ++++++++++
config/application.rb | 3 +
config/locales/en.yml | 19 +++
config/locales/fr.yml | 19 +++
.../20240412112810_add_weight_to_scopes.rb | 5 +
db/schema.rb | 1 +
.../admin/scopes_controller_extends.rb | 48 +++++++
.../decidim/scopes_controller_extends.rb | 36 ++++++
.../check_boxes_tree_helper_extends.rb | 59 +++++++++
.../decidim/check_boxes_tree_helper_spec.rb | 119 ++++++++++++++++++
.../admin_manages_organization_scopes_spec.rb | 111 ++++++++++++++++
19 files changed, 658 insertions(+)
create mode 100644 app/commands/admin/reorder_scopes.rb
create mode 100644 app/forms/decidim/user_interest_scope_form.rb
create mode 100644 app/forms/decidim/user_interests_form.rb
create mode 100644 app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb
create mode 100644 app/packs/entrypoints/decidim_custom_scopes.scss
create mode 100644 app/packs/src/decidim/admin/reorder_scopes.js
create mode 100644 app/packs/stylesheets/decidim/scopes/_scopes-custom.scss
create mode 100644 app/views/decidim/admin/scopes/index.html.erb
create mode 100644 db/migrate/20240412112810_add_weight_to_scopes.rb
create mode 100644 lib/extends/controllers/decidim/admin/scopes_controller_extends.rb
create mode 100644 lib/extends/controllers/decidim/scopes_controller_extends.rb
create mode 100644 lib/extends/helpers/decidim/check_boxes_tree_helper_extends.rb
create mode 100644 spec/helpers/decidim/check_boxes_tree_helper_spec.rb
create mode 100644 spec/system/admin_manages_organization_scopes_spec.rb
diff --git a/app/commands/admin/reorder_scopes.rb b/app/commands/admin/reorder_scopes.rb
new file mode 100644
index 0000000000..ea2a3e43bf
--- /dev/null
+++ b/app/commands/admin/reorder_scopes.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Admin
+ class ReorderScopes < Decidim::Command
+ def initialize(organization, scope, ids)
+ @organization = organization
+ @scope = scope
+ @ids = ids
+ end
+
+ def call
+ return broadcast(:invalid) if @ids.blank?
+
+ reorder_scopes
+ broadcast(:ok)
+ end
+
+ def collection
+ @collection ||= Decidim::Scope.where(id: @ids, organization: @organization)
+ end
+
+ def reorder_scopes
+ transaction do
+ set_new_weights
+ end
+ end
+
+ def set_new_weights
+ @ids.each do |id|
+ current_scope = collection.find { |block| block.id == id.to_i }
+ next if current_scope.blank?
+
+ current_scope.update!(weight: @ids.index(id) + 1)
+ end
+ end
+ end
+end
diff --git a/app/forms/decidim/user_interest_scope_form.rb b/app/forms/decidim/user_interest_scope_form.rb
new file mode 100644
index 0000000000..319d2a3506
--- /dev/null
+++ b/app/forms/decidim/user_interest_scope_form.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Decidim
+ # The form object that handles the data behind updating a user's
+ # interests in their profile page.
+ class UserInterestScopeForm < Form
+ mimic :scope
+
+ attribute :name, JsonbAttributes
+ attribute :checked, Boolean
+ attribute :children, Array[UserInterestScopeForm]
+
+ def map_model(model_hash)
+ scope = model_hash[:scope]
+ user = model_hash[:user]
+
+ self.id = scope.id
+ self.name = scope.name
+ self.checked = user.interested_scopes_ids.include?(scope.id)
+ self.children = scope.children.sort_by(&:weight).map do |children_scope|
+ UserInterestScopeForm.from_model(scope: children_scope, user: user)
+ end
+ end
+ end
+end
diff --git a/app/forms/decidim/user_interests_form.rb b/app/forms/decidim/user_interests_form.rb
new file mode 100644
index 0000000000..6220393fa7
--- /dev/null
+++ b/app/forms/decidim/user_interests_form.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Decidim
+ # The form object that handles the data behind updating a user's
+ # interests in their profile page.
+ class UserInterestsForm < Form
+ mimic :user
+
+ attribute :scopes, Array[UserInterestScopeForm]
+
+ def newsletter_notifications_at
+ return unless newsletter_notifications
+
+ Time.current
+ end
+
+ def map_model(user)
+ self.scopes = user.organization.scopes.top_level.sort_by(&:weight).map do |scope|
+ UserInterestScopeForm.from_model(scope: scope, user: user)
+ end
+ end
+ end
+end
diff --git a/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb b/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb
new file mode 100644
index 0000000000..15a273bd7c
--- /dev/null
+++ b/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Decidim
+ module SimpleProposal
+ module ScopesHelperOverride
+ extend ActiveSupport::Concern
+
+ included do
+ def scopes_picker_field(form, name, root: false, options: { checkboxes_on_top: true, sort_by_weight: true })
+ options.merge!(selected: selected_scope(form)) if selected_scope(form)
+ form.select(name, simple_scope_options(root: root, options: options), include_blank: t("decidim.scopes.prompt"))
+ end
+
+ private
+
+ def selected_scope(form)
+ form.try(:scope_id) ||
+ form.try(:settings).try(:scope_id) ||
+ form.try(:object).try(:scope_id) ||
+ form.try(:object).try(:decidim_scope_id)
+ end
+
+ def simple_scope_options(root: false, options: {})
+ scopes_array = []
+ roots = root ? root.children : ancestors
+
+ roots.sort_by { |ancestor| ancestor.weight || 0 }.each do |ancestor|
+ children_after_parent(ancestor, scopes_array, "")
+ end
+
+ selected = options.has_key?(:selected) ? options[:selected] : params.dig(:filter, :decidim_scope_id)
+ options_for_select(scopes_array, selected)
+ end
+
+ def ancestors
+ @ancestors ||= current_organization.scopes.where(parent_id: nil)
+ end
+
+ def children_after_parent(ancestor, array, prefix)
+ array << ["#{prefix} #{translated_attribute(ancestor.name)}", ancestor.id, ancestor.weight]
+ ancestor.children.sort_by { |child| child.weight || 0 }.each do |child|
+ children_after_parent(child, array, "#{prefix}-")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/packs/entrypoints/application.js b/app/packs/entrypoints/application.js
index fc8ab3b017..1d2bfce7d8 100644
--- a/app/packs/entrypoints/application.js
+++ b/app/packs/entrypoints/application.js
@@ -17,3 +17,5 @@
// Activate Active Storage
// import * as ActiveStorage from "@rails/activestorage"
// ActiveStorage.start()
+
+import "src/decidim/admin/reorder_scopes";
diff --git a/app/packs/entrypoints/decidim_custom_scopes.scss b/app/packs/entrypoints/decidim_custom_scopes.scss
new file mode 100644
index 0000000000..eace17edd8
--- /dev/null
+++ b/app/packs/entrypoints/decidim_custom_scopes.scss
@@ -0,0 +1 @@
+@import "stylesheets/decidim/scopes/scopes-custom.scss";
\ No newline at end of file
diff --git a/app/packs/src/decidim/admin/reorder_scopes.js b/app/packs/src/decidim/admin/reorder_scopes.js
new file mode 100644
index 0000000000..b997c3c509
--- /dev/null
+++ b/app/packs/src/decidim/admin/reorder_scopes.js
@@ -0,0 +1,19 @@
+$(document).ready(() => {
+ let activeBlocks = Array.prototype.slice.call(document.querySelectorAll(".js-list-scopes li"));
+ const defaultOrder = activeBlocks.map(block => block.dataset.scopeId);
+
+ document.addEventListener("dragend", () => {
+ activeBlocks = Array.prototype.slice.call(document.querySelectorAll(".js-list-scopes li"));
+ let activeBlocksManifestName = activeBlocks.map(block => block.dataset.scopeId);
+ let sortUrl = document.querySelector(".js-list-scopes").dataset.sortUrl;
+
+ if (JSON.stringify(activeBlocksManifestName) === JSON.stringify(defaultOrder)) { return; }
+
+ $.ajax({
+ method: "PUT",
+ url: sortUrl,
+ contentType: "application/json",
+ data: JSON.stringify({ manifests: activeBlocksManifestName })
+ });
+ })
+});
\ No newline at end of file
diff --git a/app/packs/stylesheets/decidim/scopes/_scopes-custom.scss b/app/packs/stylesheets/decidim/scopes/_scopes-custom.scss
new file mode 100644
index 0000000000..fd6e3f4f1f
--- /dev/null
+++ b/app/packs/stylesheets/decidim/scopes/_scopes-custom.scss
@@ -0,0 +1,18 @@
+.draggable-list .draggable-content {
+ cursor: move;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: 600;
+ border: none !important;
+ background-color: transparent !important;
+ padding: 0.5rem 1rem;
+}
+
+.custom-text {
+ color: black;
+}
+
+.custom-list {
+ border: 1px solid lightgray !important;
+ margin: 0.4rem
+}
\ No newline at end of file
diff --git a/app/views/decidim/admin/scopes/index.html.erb b/app/views/decidim/admin/scopes/index.html.erb
new file mode 100644
index 0000000000..4b9dce24ce
--- /dev/null
+++ b/app/views/decidim/admin/scopes/index.html.erb
@@ -0,0 +1,65 @@
+<% add_decidim_page_title(t("decidim.admin.titles.scopes")) %>
+
+
+
+
+
+
+ <% if parent_scope %>
+ <%= scope_breadcrumbs(parent_scope).join(" - ").html_safe %> <%= link_to t("actions.add", scope: "decidim.admin"), new_scope_scope_path(parent_scope), class: "button tiny button--title" if allowed_to? :create, :scope %><%= link_to t("actions.edit", scope: "decidim.admin"), edit_scope_path(parent_scope), class: "button tiny button--title" if allowed_to? :edit, :scope, scope: parent_scope %>
+ <% else %>
+ <%= t "decidim.admin.titles.scopes" %> <%= link_to t("actions.add", scope: "decidim.admin"), new_scope_path, class: "button tiny button--title" if allowed_to? :create, :scope %>
+ <% end %>
+
+
+
+ <% if @scopes.any? %>
+
+ <% else %>
+
<%= t("decidim.admin.scopes.no_scopes") %>
+ <% end %>
+
+
+
+
+
+
+<%= stylesheet_pack_tag "decidim_custom_scopes", media: "all" %>
+<%= javascript_pack_tag 'application' %>
\ No newline at end of file
diff --git a/config/application.rb b/config/application.rb
index 80c2f74eaa..1411333f2d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -48,7 +48,10 @@ class Application < Rails::Application
config.after_initialize do
require "extends/controllers/decidim/devise/sessions_controller_extends"
require "extends/controllers/decidim/editor_images_controller_extends"
+ require "extends/controllers/decidim/admin/scopes_controller_extends"
+ require "extends/controllers/decidim/scopes_controller_extends"
require "extends/services/decidim/iframe_disabler_extends"
+ require "extends/helpers/decidim/check_boxes_tree_helper_extends"
require "extends/helpers/decidim/icon_helper_extends"
require "extends/commands/decidim/initiatives/admin/update_initiative_answer_extends"
require "extends/controllers/decidim/initiatives/committee_requests_controller_extends"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 56be2f097d..beafaddd2b 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -16,13 +16,31 @@ en:
file: importing file
decidim:
admin:
+ actions:
+ add: Add
+ browse: Browse
+ confirm_destroy: Confirm destroy
+ destroy: Destroy
+ edit: Edit
exports:
export_as: "%{name} as %{export_format}"
notice: Your export is currently in progress. You'll receive an email when it's complete.
+ models:
+ scope:
+ fields:
+ name: Name
+ scope_type: Scope type
participatory_space_private_users:
create:
error: Error
success: Success
+ scopes:
+ no_scopes: No scopes at this level.
+ update:
+ error: There was a problem updating this scope.
+ success: Scope updated successfully
+ titles:
+ scopes: Scopes
amendments:
emendation:
announcement:
@@ -175,6 +193,7 @@ en:
change: Change
choose: Choose
currently_selected: Currently selected
+ prompt: Select a scope
shared:
login_modal:
close_modal: Close modal
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 6e725c5912..c5c91823bc 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -16,15 +16,33 @@ fr:
file: importer un fichier d'utilisateurs
decidim:
admin:
+ actions:
+ add: Ajouter
+ browse: Naviguer
+ confirm_destroy: Confirmer la suppression
+ destroy: Supprimer
+ edit: Modifier
exports:
export_as: "%{name} au format %{export_format}"
notice: Votre exportation est en cours. Vous recevrez un e-mail quand elle sera terminée.
menu:
admin_accountability: Admin accountability
+ models:
+ scope:
+ fields:
+ name: Nom
+ scope_type: Type de secteur
participatory_space_private_users:
create:
error: Erreur
success: Succès
+ scopes:
+ no_scopes: Aucun secteur à ce niveau.
+ update:
+ error: Il y a eu une erreur lors de la mise à jour du secteur.
+ success: Secteur mis à jour avec succès.
+ titles:
+ scopes: Secteurs
amendments:
emendation:
announcement:
@@ -177,6 +195,7 @@ fr:
change: Modifier
choose: Sélectionner
currently_selected: Sélectionné
+ prompt: Sélectionnez un périmètre d'application
shared:
login_modal:
close_modal: Fermer
diff --git a/db/migrate/20240412112810_add_weight_to_scopes.rb b/db/migrate/20240412112810_add_weight_to_scopes.rb
new file mode 100644
index 0000000000..bbcbc86565
--- /dev/null
+++ b/db/migrate/20240412112810_add_weight_to_scopes.rb
@@ -0,0 +1,5 @@
+class AddWeightToScopes < ActiveRecord::Migration[6.1]
+ def change
+ add_column :decidim_scopes, :weight, :integer, default: 0
+ end
+end
\ No newline at end of file
diff --git a/db/schema.rb b/db/schema.rb
index 4432bd4822..cbad410846 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1766,6 +1766,7 @@
t.string "code", null: false
t.integer "part_of", default: [], null: false, array: true
t.jsonb "geojson"
+ t.integer "weight", default: 0
t.index ["decidim_organization_id", "code"], name: "index_decidim_scopes_on_decidim_organization_id_and_code", unique: true
t.index ["decidim_organization_id"], name: "index_decidim_scopes_on_decidim_organization_id"
t.index ["parent_id"], name: "index_decidim_scopes_on_parent_id"
diff --git a/lib/extends/controllers/decidim/admin/scopes_controller_extends.rb b/lib/extends/controllers/decidim/admin/scopes_controller_extends.rb
new file mode 100644
index 0000000000..3db4a906b1
--- /dev/null
+++ b/lib/extends/controllers/decidim/admin/scopes_controller_extends.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require "active_support/concern"
+
+module Decidim
+ module Admin
+ module ScopesControllerExtends
+ extend ActiveSupport::Concern
+ included do
+ def index
+ enforce_permission_to :read, :scope
+ @scopes = children_scopes.sort_by(&:weight)
+ end
+
+ def update
+ enforce_permission_to :update, :scope, scope: scope
+ @form = form(ScopeForm).from_params(params)
+
+ return update_scopes if params[:id] == "refresh_scopes"
+
+ UpdateScope.call(scope, @form) do
+ on(:ok) do
+ flash[:notice] = I18n.t("scopes.update.success", scope: "decidim.admin")
+ redirect_to current_scopes_path
+ end
+
+ on(:invalid) do
+ flash.now[:alert] = I18n.t("scopes.update.error", scope: "decidim.admin")
+ render :edit
+ end
+ end
+ end
+
+ private
+
+ def update_scopes
+ ::Admin::ReorderScopes.call(current_organization, :scopes, params[:manifests]) do
+ on(:ok) do
+ flash[:notice] = I18n.t("scopes.update.success", scope: "decidim.admin")
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+Decidim::Admin::ScopesController.include(Decidim::Admin::ScopesControllerExtends)
diff --git a/lib/extends/controllers/decidim/scopes_controller_extends.rb b/lib/extends/controllers/decidim/scopes_controller_extends.rb
new file mode 100644
index 0000000000..62197557b9
--- /dev/null
+++ b/lib/extends/controllers/decidim/scopes_controller_extends.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require "active_support/concern"
+
+module ScopesControllerExtends
+ extend ActiveSupport::Concern
+ included do
+ def picker
+ enforce_permission_to :pick, :scope
+
+ context = picker_context(root, title, max_depth)
+ required = params&.[](:required) != "false"
+
+ scopes, parent_scopes = resolve_picker_scopes(root, current)
+
+ render(
+ :picker,
+ layout: nil,
+ locals: {
+ required: required,
+ title: title,
+ root: root,
+ current: (current || root),
+ scopes: scopes&.sort_by(&:weight),
+ parent_scopes: parent_scopes.sort_by(&:weight),
+ picker_target_id: (params[:target_element_id] || "content"),
+ global_value: params[:global_value],
+ max_depth: max_depth,
+ context: context
+ }
+ )
+ end
+ end
+end
+
+Decidim::ScopesController.include(ScopesControllerExtends)
diff --git a/lib/extends/helpers/decidim/check_boxes_tree_helper_extends.rb b/lib/extends/helpers/decidim/check_boxes_tree_helper_extends.rb
new file mode 100644
index 0000000000..9688decdf4
--- /dev/null
+++ b/lib/extends/helpers/decidim/check_boxes_tree_helper_extends.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module CheckBoxesTreeHelperExtends
+ def filter_scopes_values
+ return filter_scopes_values_from_parent(current_component.scope) if current_component.scope.present?
+
+ main_scopes = current_participatory_space.scopes.top_level
+ .includes(:scope_type, :children)
+ .sort_by(&:weight)
+ filter_scopes_values_from(main_scopes)
+ end
+
+ def filter_scopes_values_from_parent(scope)
+ scopes_values = []
+ scope.children.sort_by(&:weight).each do |child|
+ unless child.children
+ scopes_values << Decidim::CheckBoxesTreeHelper::TreePoint.new(child.id.to_s, translated_attribute(child.name, current_participatory_space.organization))
+ next
+ end
+ scopes_values << Decidim::CheckBoxesTreeHelper::TreeNode.new(
+ Decidim::CheckBoxesTreeHelper::TreePoint.new(child.id.to_s, translated_attribute(child.name, current_participatory_space.organization)),
+ scope_children_to_tree(child)
+ )
+ end
+
+ filter_tree_from(scopes_values)
+ end
+
+ def filter_scopes_values_from(scopes)
+ scopes_values = scopes.compact.sort_by(&:weight).flat_map do |scope|
+ Decidim::CheckBoxesTreeHelper::TreeNode.new(
+ Decidim::CheckBoxesTreeHelper::TreePoint.new(scope.id.to_s, translated_attribute(scope.name, current_participatory_space.organization)),
+ scope_children_to_tree(scope)
+ )
+ end
+
+ scopes_values.prepend(Decidim::CheckBoxesTreeHelper::TreePoint.new("global", t("decidim.scopes.global"))) if current_participatory_space.scope.blank?
+
+ filter_tree_from(scopes_values)
+ end
+
+ def scope_children_to_tree(scope)
+ return if scope.scope_type && scope.scope_type == current_participatory_space.try(:scope_type_max_depth)
+ return unless scope.children.any?
+
+ sorted_children = scope.children.includes(:scope_type, :children).sort_by(&:weight)
+
+ sorted_children.flat_map do |child|
+ Decidim::CheckBoxesTreeHelper::TreeNode.new(
+ Decidim::CheckBoxesTreeHelper::TreePoint.new(child.id.to_s, translated_attribute(child.name, current_participatory_space.organization)),
+ scope_children_to_tree(child)
+ )
+ end
+ end
+end
+
+Decidim::CheckBoxesTreeHelper.module_eval do
+ prepend(CheckBoxesTreeHelperExtends)
+end
diff --git a/spec/helpers/decidim/check_boxes_tree_helper_spec.rb b/spec/helpers/decidim/check_boxes_tree_helper_spec.rb
new file mode 100644
index 0000000000..df858de861
--- /dev/null
+++ b/spec/helpers/decidim/check_boxes_tree_helper_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+module Decidim
+ describe CheckBoxesTreeHelper do
+ let(:helper) do
+ Class.new(ActionView::Base) do
+ include CheckBoxesTreeHelper
+ include TranslatableAttributes
+ end.new(ActionView::LookupContext.new(ActionController::Base.view_paths), {}, [])
+ end
+
+ let!(:organization) { create(:organization) }
+ let!(:participatory_space) { create(:participatory_process, organization: organization) }
+ let!(:component) { create(:component, participatory_space: participatory_space) }
+
+ before do
+ allow(helper).to receive(:current_participatory_space).and_return(participatory_space)
+ allow(helper).to receive(:current_component).and_return(component)
+ end
+
+ describe "#filter_scopes_values" do
+ let(:root) { helper.filter_scopes_values }
+ let(:leaf) { helper.filter_scopes_values.leaf }
+ let(:nodes) { helper.filter_scopes_values.node }
+
+ context "when the participatory space does not have a scope" do
+ it "returns the global scope" do
+ expect(leaf.value).to eq("")
+ expect(nodes.count).to eq(1)
+ expect(nodes.first).to be_a(Decidim::CheckBoxesTreeHelper::TreePoint)
+ expect(nodes.first.value).to eq("global")
+ end
+ end
+
+ context "when the participatory space has a scope with subscopes" do
+ let(:participatory_space) { create(:participatory_process, :with_scope, organization: organization) }
+ let!(:subscopes) { create_list :subscope, 5, parent: participatory_space.scope }
+
+ it "returns all the subscopes" do
+ expect(leaf.value).to eq("")
+ expect(root).to be_a(Decidim::CheckBoxesTreeHelper::TreeNode)
+ expect(root.node.count).to eq(5)
+ end
+ end
+
+ context "when the component does not have a scope" do
+ before do
+ component.update!(settings: { scopes_enabled: true, scope_id: nil })
+ end
+
+ it "returns the global scope" do
+ expect(leaf.value).to eq("")
+ expect(nodes.count).to eq(1)
+ expect(nodes.first).to be_a(Decidim::CheckBoxesTreeHelper::TreePoint)
+ expect(nodes.first.value).to eq("global")
+ end
+ end
+
+ context "when the component has a scope with subscopes" do
+ let(:participatory_space) { create(:participatory_process, :with_scope, organization: organization) }
+ let!(:subscopes) { create_list :subscope, 5, parent: participatory_space.scope }
+
+ before do
+ component.update!(settings: { scopes_enabled: true, scope_id: participatory_space.scope.id })
+ end
+
+ it "returns all the subscopes" do
+ expect(leaf.value).to eq("")
+ expect(root).to be_a(Decidim::CheckBoxesTreeHelper::TreeNode)
+ expect(root.node.count).to eq(5)
+ end
+ end
+ end
+
+ context "when there is weight in the scopes" do
+ let(:participatory_space) { create(:participatory_process, :with_scope, organization: organization) }
+ let!(:subscopes) { create_list(:subscope, 5, parent: participatory_space.scope) }
+
+ before do
+ subscopes.shuffle.each_with_index { |subscope, index| subscope.update!(weight: index) }
+ end
+
+ it "returns the subscopes sorted by weight" do
+ expected_ids = subscopes.sort_by(&:weight).map { |subscope| subscope.id.to_s }
+ actual_values = helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s }
+ expect(actual_values).to eq(expected_ids)
+ end
+
+ it "assigns weights correctly after shuffle" do
+ weights = subscopes.map(&:weight)
+ expect(weights).to match_array([0, 1, 2, 3, 4])
+ end
+
+ it "sorts subscopes correctly by weight" do
+ sorted_subscopes = subscopes.sort_by(&:weight)
+ expect(subscopes.sort_by(&:weight)).to eq(sorted_subscopes)
+ end
+
+ it "checks that the helper method returns sorted subscopes" do
+ sorted_subscopes = subscopes.sort_by(&:weight).map { |subscope| subscope.id.to_s }
+ expect(helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s }).to eq(sorted_subscopes)
+ end
+
+ it "returns false when the subscopes are not sorted by weight" do
+ unsorted_subscopes = subscopes.shuffle
+ unsorted_values = unsorted_subscopes.map { |subscope| subscope.id.to_s }
+ expect(helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s }).not_to eq(unsorted_values)
+ end
+
+ it "returns false when subscopes are not sorted in ascending order of weight" do
+ reversed_subscopes = subscopes.sort_by(&:weight).reverse
+ reversed_values = reversed_subscopes.map { |subscope| subscope.id.to_s }
+ expect(helper.filter_scopes_values.node.map { |node| node.values.first.value.to_s }).not_to eq(reversed_values)
+ end
+ end
+ end
+end
diff --git a/spec/system/admin_manages_organization_scopes_spec.rb b/spec/system/admin_manages_organization_scopes_spec.rb
new file mode 100644
index 0000000000..c1e28bbc31
--- /dev/null
+++ b/spec/system/admin_manages_organization_scopes_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe "Organization scopes", type: :system do
+ include Decidim::SanitizeHelper
+
+ let(:organization) { create :organization, default_locale: :en, available_locales: [:en, :es, :ca, :fr] }
+ let(:admin) { create :user, :admin, :confirmed, organization: organization }
+ let!(:attributes) { attributes_for(:scope) }
+
+ before do
+ switch_to_host(organization.host)
+ end
+
+ describe "Managing scopes" do
+ let!(:scope_type) { create(:scope_type, organization: admin.organization) }
+
+ before do
+ login_as admin, scope: :user
+ visit decidim_admin.root_path
+ click_link "Settings"
+ click_link "Scopes"
+ end
+
+ it "can create new scopes" do
+ click_link "Add"
+
+ within ".new_scope" do
+ fill_in_i18n :scope_name, "#scope-name-tabs", **attributes[:name].except("machine_translations")
+ fill_in "Code", with: "MY-DISTRICT"
+ select scope_type.name["en"], from: :scope_scope_type_id
+
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_admin_callout("successfully")
+
+ within ".draggable-list" do
+ expect(page).to have_content(translated(attributes[:name]))
+ end
+
+ visit decidim_admin.root_path
+ expect(page).to have_content("created the #{translated(attributes[:name])} scope")
+ end
+
+ context "with existing scopes" do
+ let!(:scope) { create(:scope, organization: organization) }
+
+ before do
+ visit current_path
+ end
+
+ it "can edit them" do
+ within find(".draggable-content", text: translated(scope.name)) do
+ click_link "Edit"
+ end
+
+ within ".edit_scope" do
+ fill_in_i18n :scope_name, "#scope-name-tabs", **attributes[:name].except("machine_translations")
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_admin_callout("successfully")
+
+ within ".draggable-list" do
+ expect(page).to have_content(translated(attributes[:name]))
+ end
+
+ visit decidim_admin.root_path
+ expect(page).to have_content("updated the #{translated(attributes[:name])} scope")
+ end
+
+ it "can delete them" do
+ within find(".draggable-content", text: translated(scope.name)) do
+ accept_confirm { click_link "Destroy" }
+ end
+
+ expect(page).to have_admin_callout("successfully")
+
+ within ".card-section" do
+ expect(page).to have_no_content(translated(scope.name))
+ end
+ end
+
+ it "can create a new subcope" do
+ within find(".draggable-content", text: translated(scope.name)) do
+ find("a", text: translated(scope.name)).click
+ end
+
+ click_link "Add"
+
+ within ".new_scope" do
+ fill_in_i18n :scope_name, "#scope-name-tabs", en: "My nice subdistrict",
+ es: "Mi lindo subdistrito",
+ ca: "El meu bonic subbarri"
+ fill_in "Code", with: "MY-SUBDISTRICT"
+ select scope_type.name["en"], from: :scope_scope_type_id
+
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_admin_callout("successfully")
+
+ within ".draggable-list" do
+ expect(page).to have_content("My nice subdistrict")
+ end
+ end
+ end
+ end
+end
From 45f0c4328b85b75c2904c4da8105e4bd923e17ef Mon Sep 17 00:00:00 2001
From: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com>
Date: Mon, 25 Nov 2024 10:42:26 +0100
Subject: [PATCH 08/16] feat: Allow to choose notification settings when
attachment added (#627)
* fix: Override Create attachment admin command
* fix: Force email notification on attachment event
* fix: Override attachment form
* feat: Allow admins to toggle notification sending
* refactor: Remove override and add extend
* lint: Fix rubocop offenses
---------
Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com>
---
app/forms/decidim/admin/attachment_form.rb | 44 ++++++++
.../decidim/admin/attachments/_form.html.erb | 33 ++++++
config/application.rb | 23 ++--
config/locales/en.yml | 3 +
config/locales/fr.yml | 3 +
.../admin/create_attachment_extends.rb | 25 +++++
.../decidim/admin/create_attachment_spec.rb | 104 ++++++++++++++++++
7 files changed, 227 insertions(+), 8 deletions(-)
create mode 100644 app/forms/decidim/admin/attachment_form.rb
create mode 100644 app/views/decidim/admin/attachments/_form.html.erb
create mode 100644 lib/extends/commands/decidim/admin/create_attachment_extends.rb
create mode 100644 spec/commands/decidim/admin/create_attachment_spec.rb
diff --git a/app/forms/decidim/admin/attachment_form.rb b/app/forms/decidim/admin/attachment_form.rb
new file mode 100644
index 0000000000..c0ab45b5a6
--- /dev/null
+++ b/app/forms/decidim/admin/attachment_form.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Decidim
+ module Admin
+ # A form object used to create attachments in a participatory process.
+ #
+ class AttachmentForm < Form
+ include TranslatableAttributes
+
+ attribute :file
+ translatable_attribute :title, String
+ translatable_attribute :description, String
+ attribute :weight, Integer, default: 0
+ attribute :attachment_collection_id, Integer
+ attribute :send_notification_to_followers, Boolean, default: false
+
+ mimic :attachment
+
+ validates :file, presence: true, unless: :persisted?
+ validates :file, passthru: { to: Decidim::Attachment }
+ validates :title, :description, translatable_presence: true
+ validates :attachment_collection, presence: true, if: ->(form) { form.attachment_collection_id.present? }
+ validates :attachment_collection_id, inclusion: { in: :attachment_collection_ids }, allow_blank: true
+
+ delegate :attached_to, to: :context, prefix: false
+
+ alias organization current_organization
+
+ def attachment_collections
+ @attachment_collections ||= attached_to.attachment_collections
+ end
+
+ def attachment_collection
+ @attachment_collection ||= attachment_collections.find_by(id: attachment_collection_id)
+ end
+
+ private
+
+ def attachment_collection_ids
+ @attachment_collection_ids ||= attachment_collections.pluck(:id)
+ end
+ end
+ end
+end
diff --git a/app/views/decidim/admin/attachments/_form.html.erb b/app/views/decidim/admin/attachments/_form.html.erb
new file mode 100644
index 0000000000..1f8a2230bc
--- /dev/null
+++ b/app/views/decidim/admin/attachments/_form.html.erb
@@ -0,0 +1,33 @@
+
+
+
+ <%= title %>
+
+
+
+
+
+ <%= form.translated :text_field, :title, autofocus: true %>
+
+
+
+ <%= form.number_field :weight %>
+
+
+
+ <%= form.translated :text_field, :description %>
+
+
+
+ <%= form.select :attachment_collection_id, @form.attachment_collections.collect { |c| [translated_attribute(c.name), c.id] }, include_blank: true %>
+
+
+
+ <%= form.upload :file, required: true %>
+
+
+
+ <%= form.check_box :send_notification_to_followers, label: t(".send_notification_to_followers") %>
+
+
+
diff --git a/config/application.rb b/config/application.rb
index 1411333f2d..1e45491268 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -46,24 +46,31 @@ class Application < Rails::Application
end
config.after_initialize do
+ # Controllers
require "extends/controllers/decidim/devise/sessions_controller_extends"
require "extends/controllers/decidim/editor_images_controller_extends"
+ require "extends/controllers/decidim/proposals/proposals_controller_extends"
+ require "extends/controllers/decidim/newsletters_controller_extends"
require "extends/controllers/decidim/admin/scopes_controller_extends"
require "extends/controllers/decidim/scopes_controller_extends"
- require "extends/services/decidim/iframe_disabler_extends"
- require "extends/helpers/decidim/check_boxes_tree_helper_extends"
- require "extends/helpers/decidim/icon_helper_extends"
- require "extends/commands/decidim/initiatives/admin/update_initiative_answer_extends"
require "extends/controllers/decidim/initiatives/committee_requests_controller_extends"
+ # Models
require "extends/models/decidim/budgets/project_extends"
require "extends/models/decidim/authorization_extends"
+ require "extends/models/decidim/decidim_awesome/proposal_extra_field_extends"
+ # Services
+ require "extends/services/decidim/iframe_disabler_extends"
+ # Helpers
+ require "extends/helpers/decidim/icon_helper_extends"
+ require "extends/helpers/decidim/check_boxes_tree_helper_extends"
+ # Forms
+ require "extends/forms/decidim/initiatives/initiative_form_extends"
require "extends/forms/decidim/initiatives/admin/initiative_form_extends"
+ # Commands
+ require "extends/commands/decidim/initiatives/admin/update_initiative_answer_extends"
require "extends/commands/decidim/budgets/admin/import_proposals_to_budgets_extends"
- require "extends/controllers/decidim/newsletters_controller_extends"
require "extends/commands/decidim/admin/destroy_participatory_space_private_user_extends"
- require "extends/controllers/decidim/proposals/proposals_controller_extends"
- require "extends/forms/decidim/initiatives/initiative_form_extends"
- require "extends/models/decidim/decidim_awesome/proposal_extra_field_extends"
+ require "extends/commands/decidim/admin/create_attachment_extends"
Decidim::GraphiQL::Rails.config.tap do |config|
config.initial_query = "{\n deployment {\n version\n branch\n remote\n upToDate\n currentCommit\n latestCommit\n locallyModified\n }\n}".html_safe
diff --git a/config/locales/en.yml b/config/locales/en.yml
index beafaddd2b..89f03de8b5 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -22,6 +22,9 @@ en:
confirm_destroy: Confirm destroy
destroy: Destroy
edit: Edit
+ attachments:
+ form:
+ send_notification_to_followers: Send a notification to all the people following the consultation who have agreed to receive email notifications
exports:
export_as: "%{name} as %{export_format}"
notice: Your export is currently in progress. You'll receive an email when it's complete.
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index c5c91823bc..3bd7493ba8 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -22,6 +22,9 @@ fr:
confirm_destroy: Confirmer la suppression
destroy: Supprimer
edit: Modifier
+ attachments:
+ form:
+ send_notification_to_followers: Envoyer une notification à toutes les personnes qui suivent la concertation ayant accepté de recevoir des notifications par mail
exports:
export_as: "%{name} au format %{export_format}"
notice: Votre exportation est en cours. Vous recevrez un e-mail quand elle sera terminée.
diff --git a/lib/extends/commands/decidim/admin/create_attachment_extends.rb b/lib/extends/commands/decidim/admin/create_attachment_extends.rb
new file mode 100644
index 0000000000..ea1efbf28c
--- /dev/null
+++ b/lib/extends/commands/decidim/admin/create_attachment_extends.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require "active_support/concern"
+
+module CreateAttachmentExtends
+ extend ActiveSupport::Concern
+
+ included do
+ def notify_followers
+ return unless @attachment.attached_to.is_a?(Decidim::Followable)
+ return unless form.send_notification_to_followers
+
+ Decidim::EventsManager.publish(
+ event: "decidim.events.attachments.attachment_created",
+ event_class: Decidim::AttachmentCreatedEvent,
+ resource: @attachment,
+ followers: @attachment.attached_to.followers,
+ extra: { force_email: true },
+ force_send: true
+ )
+ end
+ end
+end
+
+Decidim::Admin::CreateAttachment.include(CreateAttachmentExtends)
diff --git a/spec/commands/decidim/admin/create_attachment_spec.rb b/spec/commands/decidim/admin/create_attachment_spec.rb
new file mode 100644
index 0000000000..35425848ae
--- /dev/null
+++ b/spec/commands/decidim/admin/create_attachment_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+module Decidim::Admin
+ describe CreateAttachment do
+ subject { described_class.call(form, attached_to, user) }
+ let(:user) { create(:user) }
+ let(:send_notification) { true }
+ let(:form) do
+ instance_double(
+ AttachmentForm,
+ title: {
+ en: "An image",
+ ca: "Una imatge",
+ es: "Una imagen"
+ },
+ description: {
+ en: "A city",
+ ca: "Una ciutat",
+ es: "Una ciudad"
+ },
+ file: file,
+ attachment_collection: nil,
+ send_notification_to_followers: send_notification,
+ weight: 0
+ )
+ end
+ let(:file) { upload_test_file(Decidim::Dev.test_file("city.jpeg", "image/jpeg")) }
+ let(:attached_to) { create(:participatory_process) }
+
+ describe "when valid" do
+ before do
+ allow(form).to receive(:invalid?).and_return(false)
+ end
+
+ it "broadcasts :ok and creates the component" do
+ expect do
+ subject
+ end.to broadcast(:ok)
+
+ expect(Decidim::Attachment.count).to eq(1)
+ end
+
+ it "notifies the followers" do
+ follower = create(:user, organization: attached_to.organization)
+ create(:follow, followable: attached_to, user: follower)
+
+ expect(Decidim::EventsManager)
+ .to receive(:publish)
+ .with(
+ event: "decidim.events.attachments.attachment_created",
+ event_class: Decidim::AttachmentCreatedEvent,
+ resource: kind_of(Decidim::Attachment),
+ followers: [follower],
+ extra: { force_email: true },
+ force_send: true
+ )
+
+ subject
+ end
+
+ context "when send notification option is false" do
+ let(:send_notification) { false }
+
+ it "does not notify the followers" do
+ follower = create(:user, organization: attached_to.organization)
+ create(:follow, followable: attached_to, user: follower)
+
+ expect(Decidim::EventsManager)
+ .not_to receive(:publish)
+
+ subject
+ end
+ end
+
+ it "traces the action", versioning: true do
+ expect(Decidim.traceability)
+ .to receive(:perform_action!)
+ .with(:create, Decidim::Attachment, user)
+ .and_call_original
+
+ expect { subject }.to change(Decidim::ActionLog, :count)
+ action_log = Decidim::ActionLog.last
+ expect(action_log.action).to eq("create")
+ expect(action_log.version).to be_present
+ end
+ end
+
+ describe "when invalid" do
+ before do
+ allow(form).to receive(:invalid?).and_return(true)
+ end
+
+ it "broadcasts invalid" do
+ expect do
+ subject
+ end.to broadcast(:invalid)
+
+ expect(Decidim::Attachment.count).to eq(0)
+ end
+ end
+ end
+end
From e1e9dce2a1b3eaf00e1d5ccf8f82d1123dc861f7 Mon Sep 17 00:00:00 2001
From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com>
Date: Wed, 27 Nov 2024 17:42:31 +0100
Subject: [PATCH 09/16] fix: Remove caching from the geocoding elements to
avoid map not reloading when refreshing (#638)
---
.../proposals/proposals/index.html.erb | 38 +++++++++----------
1 file changed, 18 insertions(+), 20 deletions(-)
diff --git a/app/views/decidim/proposals/proposals/index.html.erb b/app/views/decidim/proposals/proposals/index.html.erb
index 5bcaade268..d6ab9a56ed 100644
--- a/app/views/decidim/proposals/proposals/index.html.erb
+++ b/app/views/decidim/proposals/proposals/index.html.erb
@@ -1,30 +1,28 @@
<%= render partial: "decidim/shared/component_announcement" %>
<% if component_settings.geocoding_enabled? %>
- <% cache @all_geocoded_proposals do %>
- <%= dynamic_map_for proposals_data_for_map(@all_geocoded_proposals) do %>
-