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? %> +
+ + + + + + + + +
<%= t("models.scope.fields.name", scope: "decidim.admin") %><%= t("models.scope.fields.scope_type", scope: "decidim.admin") %>
+ + +
    "> + <% @scopes.each do |scope| %> +
  • +
    +
    + <%= icon "move", class: "icon--small", role: "img", "aria-hidden": true %> + <%= link_to translated_attribute(scope.name), scope_scopes_path(scope), class:"custom-text" %> +
    +
    + <%= icon_link_to "zoom-in", scope_scopes_path(scope), t("actions.browse", scope: "decidim.admin"), class: "action-icon--browse", method: :get, data: {} %> + + <% if allowed_to? :update, :scope, scope: scope %> + <%= icon_link_to "pencil", [:edit, scope], t("actions.edit", scope: "decidim.admin"), class: "action-icon--edit", method: :get, data: {} %> + <% end %> + + <% if allowed_to? :destroy, :scope, scope: scope %> + <%= icon_link_to "circle-x", scope, t("actions.destroy", scope: "decidim.admin"), class: "action-icon--remove", method: :delete, data: { confirm: t("actions.confirm_destroy", scope: "decidim.admin") } %> + <% end %> +
    +
    +
  • + <% end %> +
+ +
+
+ <% 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 %> - <% end %> <% end %> <%= render partial: "voting_rules" %> From c7b0a9e17d54f6ed85836a9c376f478b4bedd3bf Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:57:13 +0100 Subject: [PATCH 10/16] backport: Reorder scopes in meetings (#639) --- config/application.rb | 1 + config/locales/en.yml | 3 ++ config/locales/fr.yml | 3 ++ .../directory/application_helper_extends.rb | 39 ++++++++++++++ .../directory/application_helper_spec.rb | 53 +++++++++++++++++++ 5 files changed, 99 insertions(+) create mode 100644 lib/extends/helpers/decidim/meetings/directory/application_helper_extends.rb create mode 100644 spec/helpers/decidim/meetings/directory/application_helper_spec.rb diff --git a/config/application.rb b/config/application.rb index 1e45491268..26a54c15b8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -61,6 +61,7 @@ class Application < Rails::Application # Services require "extends/services/decidim/iframe_disabler_extends" # Helpers + require "extends/helpers/decidim/meetings/directory/application_helper_extends" require "extends/helpers/decidim/icon_helper_extends" require "extends/helpers/decidim/check_boxes_tree_helper_extends" # Forms diff --git a/config/locales/en.yml b/config/locales/en.yml index 89f03de8b5..6eaf94bee8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -145,6 +145,9 @@ en: see_all_initiatives: See all initiatives unavailable_scope: Unavailable scope meetings: + application_helper: + filter_scope_values: + all: All directory: meetings: index: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 3bd7493ba8..d25c7083c1 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -147,6 +147,9 @@ fr: see_all_initiatives: Voir toutes les pétitions unavailable_scope: Portée indisponible meetings: + application_helper: + filter_scope_values: + all: Tous directory: meetings: index: diff --git a/lib/extends/helpers/decidim/meetings/directory/application_helper_extends.rb b/lib/extends/helpers/decidim/meetings/directory/application_helper_extends.rb new file mode 100644 index 0000000000..60e56f1f02 --- /dev/null +++ b/lib/extends/helpers/decidim/meetings/directory/application_helper_extends.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "active_support/concern" +module ApplicationHelperExtends + extend ActiveSupport::Concern + include Decidim::CheckBoxesTreeHelper + + included do + def directory_filter_scopes_values + main_scopes = current_organization.scopes.top_level + scopes_values = main_scopes.includes(:scope_type, :children).sort_by(&:weight).flat_map do |scope| + TreeNode.new( + TreePoint.new(scope.id.to_s, translated_attribute(scope.name, current_organization)), + scope_children_to_tree(scope) + ) + end + + scopes_values.prepend(TreePoint.new("global", t("decidim.scopes.global"))) + + TreeNode.new( + TreePoint.new("", t("decidim.meetings.application_helper.filter_scope_values.all")), + scopes_values + ) + end + + def scope_children_to_tree(scope) + return unless scope.children.any? + + scope.children.includes(:scope_type, :children).sort_by(&:weight).flat_map do |child| + TreeNode.new( + TreePoint.new(child.id.to_s, translated_attribute(child.name, current_organization)), + scope_children_to_tree(child) + ) + end + end + end +end + +Decidim::Meetings::Directory::ApplicationHelper.include(ApplicationHelperExtends) diff --git a/spec/helpers/decidim/meetings/directory/application_helper_spec.rb b/spec/helpers/decidim/meetings/directory/application_helper_spec.rb new file mode 100644 index 0000000000..fc0075add0 --- /dev/null +++ b/spec/helpers/decidim/meetings/directory/application_helper_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Meetings + module Directory + describe ApplicationHelper do + let(:helper) do + Class.new(ActionView::Base) do + include ApplicationHelper + include CheckBoxesTreeHelper + include TranslatableAttributes + end.new(ActionView::LookupContext.new(ActionController::Base.view_paths), {}, []) + end + let!(:organization) { create(:organization) } + let!(:parent_scope) { create(:scope, organization: organization) } + let!(:scope_one) { create(:scope, organization: organization, parent: parent_scope, weight: 1) } + let!(:scope_two) { create(:scope, organization: organization, parent: parent_scope, weight: 2) } + let!(:scope_three) { create(:scope, organization: organization, parent: parent_scope, weight: 3) } + + before do + allow(helper).to receive(:current_organization).and_return(organization) + end + + describe "#directory_filter_scopes_values" do + let(:root) { helper.directory_filter_scopes_values } + let(:leaf) { root.leaf } + let(:nodes) { root.node } + + context "when the organization has a scope with children" do + it "returns all the children ordered by weight" do + expect(root).to be_a(Decidim::CheckBoxesTreeHelper::TreeNode) + expect(nodes.last.node.count).to eq(3) + expect(nodes.last.node.first.leaf.label).to eq(scope_one.name["en"]) + expect(nodes.last.node.last.leaf.label).to eq(scope_three.name["en"]) + end + + context "and the weight of scope's children changes" do + it "returns the children ordered by the new weight" do + scope_one.update(weight: 4) + expect(root).to be_a(Decidim::CheckBoxesTreeHelper::TreeNode) + expect(nodes.last.node.count).to eq(3) + expect(nodes.last.node.first.leaf.label).to eq(scope_two.name["en"]) + expect(nodes.last.node.last.leaf.label).to eq(scope_one.name["en"]) + end + end + end + end + end + end + end +end From 536ad009b939128f857cc7b1c0b1d204d5e56300 Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:37:03 +0100 Subject: [PATCH 11/16] fix: Scopes can't be updated in BO (#640) Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com> --- .../decidim/simple_proposal/scopes_helper_override.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb b/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb index 15a273bd7c..2654d05639 100644 --- a/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb +++ b/app/helpers/concerns/decidim/simple_proposal/scopes_helper_override.rb @@ -4,9 +4,8 @@ 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 }) + def scopes_picker_field(form, name, root: false, options: { checkboxes_on_top: 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 @@ -23,11 +22,9 @@ def selected_scope(form) 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 @@ -37,7 +34,7 @@ def ancestors end def children_after_parent(ancestor, array, prefix) - array << ["#{prefix} #{translated_attribute(ancestor.name)}", ancestor.id, ancestor.weight] + array << ["#{prefix} #{translated_attribute(ancestor.name)}", ancestor.id] ancestor.children.sort_by { |child| child.weight || 0 }.each do |child| children_after_parent(child, array, "#{prefix}-") end From cded3f99a7faa013b7f53d2bc73edf8b8727912f Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:36:44 +0100 Subject: [PATCH 12/16] backport: Add layer of security and download p7zip-full lib on docker (#643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bump: Bump custom proposal states (#599) * Feat: custom sort for processes (#596) * feat: add custom sort for processesdepending on new variable * feat: update locales files * feat: update seeds * test: add controller tests for assemblies and processes * chore: update i18n config for unused keys * docs: update overrides * refactor: update env variable after review * Fix form initiatives (#600) * fix: XSS vulnerability with img on initiative form and model * test: add tests for new validation * docs: update overrides section * fix: interference from added extends with migration * style: update with rubocop * fix: ActiveRecord::NoDatabaseError * fix: trying to fix again interference * fix: update initiative fomr extends and modify admin initiative controller * refactor: update with rubocop * fix: validation in initiative_form extends and update test * docs: update overrides section in overloads.md * fix: Update OVERLOADS.md --------- Co-authored-by: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> * bump: Decidim-Awesome to last commit (#607) * feat: Bump decidim-awesome to last commit * fix: Fix migration that has been changed since first implementation * fix: Add Referrer-Policy to strict (#613) * fix: Flash message on proposal limit per user reached (#609) * fix error message displaying when reaching proposition add limit * add test * add keys in ignore_missing keys * fix: Add block reported user task (#614) * feat: Add module decidim-cleaner (#597) * feat: Add decidim-cleaner * refactor: Comment env var by default --------- Co-authored-by: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> * Install GuestMeetingRegistration module (#615) * Install GuestMeetingRegistration module * Update registration module * feat: Add Sendethics possibility to the sms gateway (#605) Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com> * Bump: Phone authorization handler module (#623) * backport: remove sentry (#622) * backport: Use cdn (#624) * backport: self hosted cdn * backport: update js files content * fix: Missing image in survey question (#621) * fix: update condition to not empty input value if image is present * test: add system test to check for input value * test: update check for image * test: update img check again * test: last update check img * test: update * test: another update * test: if img is present * test: update other test to avoid ambiguous selector error * test: update to see if image is presnet after save * fix: override editor js in decidim_awesome * test: update system test * fix: Questions order in survey export (#618) * chore: update after pull * fix: order questions by position in serializer * test: add test for question order * refactor: update test * style: remove empty line * bump: Guest meeting registration module (#625) * Bump: Guest meeting registration module * refactor(Gemfile): Remove ref reference in Gemfile --------- Co-authored-by: Quentin Champenois * feat: add decypted private body to extra fields (#608) * feat: add new column to proposal extra fields * feat: add callback to proposal extra field model * test: add test for proposal extra field model * feat: add rake task to update existing data * test: add test for new rake task * feat: add proposal extra field model extends to config * chore: update rubocop rules * refactor: update task and test * create the job file * update rake task * lint code * add tests file * fix rspec * clean double specs * update spec * update spec * update syntax test with a context instade of only 'it' * lint code by removing useless line in job spec file * update spec * add more context in spec file * lint code * update rake tasks test --------- Co-authored-by: barbara oliveira Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com> Co-authored-by: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> * feat: Clear minio s3 bucket (#612) * feat(Docker): Add minio service * feat(rake): Add new tasks to cleanup s3 bucket * fix: Add S3 purge rake task * fix: S3 Bucket endpoint for docker local * fix(rake): Active storage clear orphans job * fix(sidekiq): Add sidekiq configuration * fix: Logger for active_storage.rake job * fix: Prevent duplicated ActiveRecord Query * fix: Prevent error on PP sort with end_date nil (#626) * fix: update to handle processes without start_date or end_date * fix: update sort in controllers * test: update test with process without end date * refactor: optimize queries in controllers and update tests * refactor: update sort processes in controllers --------- Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com> * bump: Module Spam Detection to 4.1.2 (#630) * feat: Author notification on proposal publication (#620) * add notification with eventmanager * base to watch the CI and see files on github * fix translation key & notififaction displaying * add send_pubication_notification to right file * start test rspec * continuing rspec * potential final test file * fix: Merge proposal command and anonymous proposals * fix: Change ProposalPublishedEvent to SimpleEvent * fix: Proposal Published Event * fix: Push FR locales * test: Add specs for proposal_published_event * update methode & file name * adjust trad key * adjust test file & update name of test file * lint * lint fr trad key * lint * update trad key order * lint * correct trad key link in method * correst rspec * fix rspec * fix: Notification small title * lint(rubocop): Fix offenses * ci: Exclude BeEq Rubocop rule * clean * fix CI * delete test file * add ignore trad key in i118n-tasks.yml to fix CI * update text syntaxe via trad key * add '' in fr trad key --------- Co-authored-by: Quentin Champenois * revert: "fix: Flash message on proposal limit per user reached (#609)" (#634) This reverts commit 28003b5d45a89c7233596c4b751b1dabdf7690aa. * bump: Fix geocofing on homepage interactive map (#635) * feat: Add module emitter (#633) * feat: Addition of the emitter module * fix: Fix failing specs * 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 * 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 * 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> * fix: Remove caching from the geocoding elements to avoid map not reloading when refreshing (#638) * backport: Reorder scopes in meetings (#639) * fix: Scopes can't be updated in BO (#640) Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com> * backport: Add layer of security and download p7zip-full lib on docker --------- Co-authored-by: stephanierousset <61418966+Stef-Rousset@users.noreply.github.com> Co-authored-by: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> Co-authored-by: ’Barbara Oliveira <143180473+BarbaraOliveira13@users.noreply.github.com> Co-authored-by: Quentin Champenois Co-authored-by: Alexandru Emil Lupu Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com> Co-authored-by: barbara oliveira --- Dockerfile | 4 ++-- Dockerfile.local | 4 ++-- app/services/decidim/download_your_data_exporter.rb | 8 ++++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index f62346cd5b..63ec05e32e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ENV RAILS_ENV=production \ WORKDIR /app RUN apt-get update && \ - apt-get -y install libpq-dev curl git libicu-dev build-essential && \ + apt-get -y install libpq-dev curl git libicu-dev build-essential p7zip-full && \ curl https://deb.nodesource.com/setup_16.x | bash && \ apt-get install -y nodejs && \ npm install --global yarn && \ @@ -41,7 +41,7 @@ ENV RAILS_ENV=production \ RAILS_LOG_TO_STDOUT=true RUN apt update && \ - apt install -y postgresql-client imagemagick libproj-dev proj-bin libjemalloc2 && \ + apt install -y postgresql-client imagemagick libproj-dev proj-bin libjemalloc2 p7zip-full && \ gem install bundler:2.4.9 WORKDIR /app diff --git a/Dockerfile.local b/Dockerfile.local index 8a5452123b..20fbdc5c7c 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -8,7 +8,7 @@ ENV RAILS_ENV=production \ # Install common dependencies RUN apt-get update -q && \ apt-get install -yq --no-install-recommends \ - libpq-dev curl git libicu-dev build-essential openssl && \ + libpq-dev curl git libicu-dev build-essential openssl p7zip-full && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* @@ -66,7 +66,7 @@ WORKDIR /app # Install runtime dependencies RUN apt-get update -q && \ apt-get install -yq --no-install-recommends \ - postgresql-client imagemagick libproj-dev proj-bin libjemalloc2 && \ + postgresql-client imagemagick libproj-dev proj-bin libjemalloc2 p7zip-full && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* diff --git a/app/services/decidim/download_your_data_exporter.rb b/app/services/decidim/download_your_data_exporter.rb index 3cb8432f0c..893d16e383 100644 --- a/app/services/decidim/download_your_data_exporter.rb +++ b/app/services/decidim/download_your_data_exporter.rb @@ -30,7 +30,7 @@ def export save_user_data(tmpdir, user_data) save_user_attachments(tmpdir, user_attachments) - SevenZipWrapper.compress_and_encrypt(filename: @path, password: @password, input_directory: tmpdir) + Decidim::SevenZipWrapper.compress_and_encrypt(filename: @path, password: @password, input_directory: tmpdir) end private @@ -59,6 +59,9 @@ def save_user_data(tmpdir, user_data) next if exporter_data.read == "\n" file_name = File.join(tmpdir, "#{entity}-#{exporter_data.filename}") + + dir_path = File.dirname(file_name) + FileUtils.mkdir_p(dir_path) unless Dir.exist?(dir_path) File.write(file_name, exporter_data.read) end end @@ -70,7 +73,8 @@ def save_user_attachments(tmpdir, user_attachments) blobs = attachment.is_a?(ActiveStorage::Attached::One) ? [attachment.blob] : attachment.blobs blobs.each do |blob| - Dir.mkdir(File.join(tmpdir, entity.parameterize)) + dir_path = File.join(tmpdir, entity.parameterize) + Dir.mkdir(dir_path) unless Dir.exist?(dir_path) file_name = File.join(tmpdir, entity.parameterize, blob.filename.to_s) blob.open do |blob_file| File.write(file_name, blob_file.read.force_encoding("UTF-8")) From d8b0fd2c95e54011b089cae29c2c8ffe7490d4e0 Mon Sep 17 00:00:00 2001 From: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:52:58 +0100 Subject: [PATCH 13/16] fix(smtp): Add authentication plain text only if user_name & password (#645) --- config/environments/production.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 8b5b1e8708..c738c04ee4 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -94,16 +94,18 @@ config.action_mailer.default_url_options = { port: 3000 } else config.action_mailer.delivery_method = :smtp - config.action_mailer.smtp_settings = { + smtp_settings = { address: Rails.application.secrets.smtp_address, port: Rails.application.secrets.smtp_port, - authentication: Rails.application.secrets.smtp_authentication, user_name: Rails.application.secrets.smtp_username, password: Rails.application.secrets.smtp_password, domain: Rails.application.secrets.smtp_domain, enable_starttls_auto: Rails.application.secrets.smtp_starttls_auto, openssl_verify_mode: "none" } + smtp_settings = smtp_settings.merge(authentication: Rails.application.secrets.smtp_authentication) if smtp_settings[:user_name].present? && smtp_settings[:password].present? + + config.action_mailer.smtp_settings = smtp_settings if Rails.application.secrets.sendgrid config.action_mailer.default_options = { From 2b16f67102e073ad71010eb64ed6b2284550f3af Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:28:12 +0100 Subject: [PATCH 14/16] fix: Fix characters limits when editing comments (#646) * fix: Fix the editing of comments to put the same chars limit than it has on its creation * fix: Add missing locales --- config/application.rb | 2 + config/locales/en.yml | 4 + config/locales/fr.yml | 4 + .../decidim/comments/comments_controller.rb | 61 +++++ .../decidim/comments/comment_form_extends.rb | 15 ++ .../comments/comments_controller_spec.rb | 237 ++++++++++++++++++ spec/forms/comment_form_spec.rb | 115 +++++++++ 7 files changed, 438 insertions(+) create mode 100644 lib/extends/controllers/decidim/comments/comments_controller.rb create mode 100644 lib/extends/forms/decidim/comments/comment_form_extends.rb create mode 100644 spec/controllers/decidim/comments/comments_controller_spec.rb create mode 100644 spec/forms/comment_form_spec.rb diff --git a/config/application.rb b/config/application.rb index 26a54c15b8..759eb48b0d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -54,6 +54,7 @@ class Application < Rails::Application require "extends/controllers/decidim/admin/scopes_controller_extends" require "extends/controllers/decidim/scopes_controller_extends" require "extends/controllers/decidim/initiatives/committee_requests_controller_extends" + require "extends/controllers/decidim/comments/comments_controller" # Models require "extends/models/decidim/budgets/project_extends" require "extends/models/decidim/authorization_extends" @@ -67,6 +68,7 @@ class Application < Rails::Application # Forms require "extends/forms/decidim/initiatives/initiative_form_extends" require "extends/forms/decidim/initiatives/admin/initiative_form_extends" + require "extends/forms/decidim/comments/comment_form_extends" # Commands require "extends/commands/decidim/initiatives/admin/update_initiative_answer_extends" require "extends/commands/decidim/budgets/admin/import_proposals_to_budgets_extends" diff --git a/config/locales/en.yml b/config/locales/en.yml index 6eaf94bee8..029ad64054 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -81,6 +81,10 @@ en: projects_count: one: 1 project other: "%{count} projects" + comments: + comments: + create: + error: There was a problem creating the comment. devise: sessions: new: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index d25c7083c1..f50ecd0449 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -83,6 +83,10 @@ fr: projects_count: one: 1 projet other: "%{count} projets" + comments: + comments: + create: + error: Une erreur s'est produite lors du vote sur le commentaire. devise: sessions: new: diff --git a/lib/extends/controllers/decidim/comments/comments_controller.rb b/lib/extends/controllers/decidim/comments/comments_controller.rb new file mode 100644 index 0000000000..5758d5eaf4 --- /dev/null +++ b/lib/extends/controllers/decidim/comments/comments_controller.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "active_support/concern" +module CommentsControllerExtends + extend ActiveSupport::Concern + included do + def update + set_comment + @commentable = comment.commentable + enforce_permission_to :update, :comment, comment: comment + + form = Decidim::Comments::CommentForm.from_params( + params.merge(commentable: comment.commentable, current_component: current_component) + ).with_context( + current_organization: current_organization + ) + + Decidim::Comments::UpdateComment.call(comment, current_user, form) do + on(:ok) do + respond_to do |format| + format.js { render :update } + end + end + + on(:invalid) do + respond_to do |format| + format.js { render :update_error } + end + end + end + end + + def create + enforce_permission_to :create, :comment, commentable: commentable + + form = Decidim::Comments::CommentForm.from_params( + params.merge(commentable: commentable, current_component: current_component) + ).with_context( + current_organization: current_organization, + current_component: current_component + ) + Decidim::Comments::CreateComment.call(form, current_user) do + on(:ok) do |comment| + handle_success(comment) + respond_to do |format| + format.js { render :create } + end + end + + on(:invalid) do + @error = t("create.error", scope: "decidim.comments.comments") + respond_to do |format| + format.js { render :error } + end + end + end + end + end +end + +Decidim::Comments::CommentsController.include(CommentsControllerExtends) diff --git a/lib/extends/forms/decidim/comments/comment_form_extends.rb b/lib/extends/forms/decidim/comments/comment_form_extends.rb new file mode 100644 index 0000000000..e8621043fe --- /dev/null +++ b/lib/extends/forms/decidim/comments/comment_form_extends.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module CommentFormExtends + extend ActiveSupport::Concern + + included do + attribute :current_component, Decidim::Component + + validates :current_component, presence: true + end +end + +Decidim::Comments::CommentForm.include(CommentFormExtends) diff --git a/spec/controllers/decidim/comments/comments_controller_spec.rb b/spec/controllers/decidim/comments/comments_controller_spec.rb new file mode 100644 index 0000000000..0920d705bd --- /dev/null +++ b/spec/controllers/decidim/comments/comments_controller_spec.rb @@ -0,0 +1,237 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Comments + describe CommentsController, type: :controller do + routes { Decidim::Comments::Engine.routes } + + let(:organization) { create(:organization) } + let(:participatory_process) { create :participatory_process, organization: organization } + let(:component) { create(:component, participatory_space: participatory_process) } + let(:commentable) { create(:dummy_resource, component: component) } + + before do + request.env["decidim.current_organization"] = organization + end + + describe "GET index" do + it "renders the index template" do + get :index, xhr: true, params: { commentable_gid: commentable.to_signed_global_id.to_s } + expect(subject).to render_template(:index) + end + + it "tells devise not to reset timeout counter" do + expect(request.env["devise.skip_timeoutable"]).to be_nil + get :index, xhr: true, params: { commentable_gid: commentable.to_signed_global_id.to_s } + expect(request.env["devise.skip_timeoutable"]).to be(true) + end + + context "when requested without an XHR request" do + it "redirects to the commentable" do + get :index, params: { commentable_gid: commentable.to_signed_global_id.to_s } + expect(subject).to redirect_to( + Decidim::ResourceLocatorPresenter.new(commentable).path + ) + end + end + + context "when the reload parameter is given" do + it "renders the reload template" do + get :index, xhr: true, params: { commentable_gid: commentable.to_signed_global_id.to_s, reload: 1 } + expect(subject).to render_template(:reload) + end + end + + context "when comments are disabled for the component" do + let(:component) { create(:component, :with_comments_disabled, participatory_space: participatory_process) } + + it "redirects with a flash alert" do + get :index, xhr: true, params: { commentable_gid: commentable.to_signed_global_id.to_s } + expect(flash[:alert]).to be_present + expect(response).to have_http_status(:redirect) + end + end + end + + describe "POST create" do + let(:comment_alignment) { 0 } + let(:comment_params) do + { + commentable_gid: commentable.to_signed_global_id.to_s, + body: "This is a new comment", + alignment: comment_alignment + } + end + + it "responds with unauthorized status" do + post :create, xhr: true, params: { comment: comment_params } + expect(response).to have_http_status(:unauthorized) + end + + context "when the user is signed in" do + let(:user) { create(:user, :confirmed, locale: "en", organization: organization) } + let(:comment) { Decidim::Comments::Comment.last } + + before do + sign_in user, scope: :user + end + + it "creates the comment" do + expect do + post :create, xhr: true, params: { comment: comment_params } + end.to change(Decidim::Comments::Comment, :count).by(1) + + expect(comment.body.values.first).to eq("This is a new comment") + expect(comment.alignment).to eq(comment_alignment) + expect(subject).to render_template(:create) + end + + context "when requested without an XHR request" do + it "throws an unknown format exception" do + expect do + post :create, params: { comment: comment_params } + end.to raise_error(ActionController::UnknownFormat) + end + end + + context "when comments are disabled for the component" do + let(:component) { create(:component, :with_comments_disabled, participatory_space: participatory_process) } + + it "redirects with a flash alert" do + post :create, xhr: true, params: { comment: comment_params } + expect(flash[:alert]).to be_present + expect(response).to have_http_status(:redirect) + end + end + + context "when trying to comment on a private space where the user is not assigned to" do + let(:participatory_process) { create :participatory_process, :private, organization: organization } + + it "redirects with a flash alert" do + post :create, xhr: true, params: { comment: comment_params } + expect(flash[:alert]).to be_present + expect(response).to have_http_status(:redirect) + end + end + + context "when comment alignment is positive" do + let(:comment_alignment) { 1 } + + it "creates the comment with the alignment defined as 1" do + expect do + post :create, xhr: true, params: { comment: comment_params } + end.to change(Decidim::Comments::Comment, :count).by(1) + + expect(comment.alignment).to eq(comment_alignment) + expect(subject).to render_template(:create) + end + end + + context "when comment alignment is negative" do + let(:comment_alignment) { -1 } + + it "creates the comment with the alignment defined as -1" do + expect do + post :create, xhr: true, params: { comment: comment_params } + end.to change(Decidim::Comments::Comment, :count).by(1) + + expect(comment.alignment).to eq(comment_alignment) + expect(subject).to render_template(:create) + end + end + + context "when comment body is missing" do + let(:comment_params) do + { + commentable_gid: commentable.to_signed_global_id.to_s, + alignment: comment_alignment + } + end + + it "renders the error template" do + post :create, xhr: true, params: { comment: comment_params } + expect(subject).to render_template(:error) + end + + context "when requested without an XHR request" do + it "throws an unknown format exception" do + expect do + post :create, params: { comment: comment_params } + end.to raise_error(ActionController::UnknownFormat) + end + end + end + + context "when comment alignment is invalid" do + let(:comment_alignment) { 2 } + + it "renders the error template" do + post :create, xhr: true, params: { comment: comment_params } + expect(subject).to render_template(:error) + end + end + + context "when the comment does not exist" do + let(:comment_params) do + { + commentable_gid: "unexisting", + body: "This is a new comment", + alignment: 0 + } + end + + it "raises a routing error" do + expect do + post :create, xhr: true, params: { comment: comment_params } + end.to raise_error(ActionController::RoutingError) + end + end + end + end + + describe "DELETE destroy" do + let(:user) { create(:user, :confirmed, locale: "en", organization: organization) } + let(:comment_author) { create(:user, :confirmed, locale: "en", organization: organization) } + let!(:comment) { create(:comment, commentable: commentable, author: comment_author) } + + it "redirects to sign in path" do + expect do + delete :destroy, xhr: true, params: { id: comment.id } + end.not_to(change { Decidim::Comments::Comment.not_deleted.count }) + + expect(response).to redirect_to("/users/sign_in") + end + + context "when a user different of the author is signed in" do + before do + sign_in user, scope: :user + end + + it "doesn't delete the comment" do + expect do + delete :destroy, xhr: true, params: { id: comment.id } + end.not_to(change { Decidim::Comments::Comment.not_deleted.count }) + + expect(response).not_to have_http_status(:success) + end + end + + context "when the author is signed in" do + before do + sign_in comment_author, scope: :user + end + + it "deletes the comment" do + expect do + delete :destroy, xhr: true, params: { id: comment.id } + end.to change { Decidim::Comments::Comment.not_deleted.count }.by(-1) + + expect(response).to have_http_status(:success) + end + end + end + end + end +end diff --git a/spec/forms/comment_form_spec.rb b/spec/forms/comment_form_spec.rb new file mode 100644 index 0000000000..f68a5c85c4 --- /dev/null +++ b/spec/forms/comment_form_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Comments + describe CommentForm do + subject do + described_class.from_params( + attributes + ).with_context( + current_organization: organization, + current_component: component + ) + end + + let(:organization) { create(:organization) } + let!(:component) { create(:component, organization: organization) } + let(:body) { "This is a new comment" } + let(:alignment) { 1 } + let(:user_group) { create(:user_group, :verified) } + let(:user_group_id) { user_group.id } + + let(:commentable) { create :dummy_resource } + + let(:attributes) do + { + "comment" => { + "body" => body, + "alignment" => alignment, + "user_group_id" => user_group_id, + "commentable" => commentable, + "current_component" => component + } + } + end + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when body is blank" do + let(:body) { "" } + + it { is_expected.not_to be_valid } + end + + context "when body is too long" do + let(:body) { "c" * 1001 } + + it { is_expected.not_to be_valid } + + context "with carriage return characters that cause it to exceed" do + let(:body) { "#{"c" * 500}\r\n#{"c" * 499}" } + + it { is_expected.to be_valid } + end + end + + context "when alignment is not present" do + let(:alignment) { nil } + + it { is_expected.to be_valid } + end + + context "when alignment is present and it is different from 0, 1 and -1" do + let(:alignment) { 2 } + + it { is_expected.not_to be_valid } + end + + context "when current_component is missing" do + let!(:component) { nil } + + it { is_expected.not_to be_valid } + end + + describe "#max_length" do + context "when organization has a max length > 0" do + let(:body) { "c" * 1001 } + let(:organization) { create(:organization, comments_max_length: 1001) } + + it { is_expected.to be_valid } + end + + context "when component has a max length > 0" do + let(:body) { "c" * 1001 } + + before do + component.update!(settings: { comments_max_length: 1001 }) + end + + it { is_expected.to be_valid } + end + + context "when component is missing" do + let!(:component) { nil } + let(:body) { "c" * 1000 } + + it { is_expected.not_to be_valid } + end + + context "when the component settings do not define comments_max_length" do + let(:organization) { create(:organization, comments_max_length: 3549) } + let(:settings) { double } + + it "returns the organization comments_max_length" do + allow(component).to receive(:settings).and_return(settings) + expect(subject.max_length).to eq(organization.comments_max_length) + end + end + end + end + end +end From 2a32714f73847bf7ec310cb53cddef6df76446b9 Mon Sep 17 00:00:00 2001 From: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:20:09 +0100 Subject: [PATCH 15/16] fix: Clear papertrail versions in database (#647) * fix(job): Papertrail versions job * fix(rake): Create rake task and sidekiq task * fix(rake): Add log information * fix(rake): Add env var to rake task * fix(job): Add versions to remove * refactor: Rename variable expiration to retention * refactor: Use Rails secrets * fix: Add DB size in system view * fix(system): Add Decidim version in system view * fix(job): Default batch size to 5000 * fix: System dashboard locales --- .../decidim/system/dashboard_controller.rb | 28 ++++++++ app/jobs/concerns/decidim/logging.rb | 17 +++++ app/jobs/decidim/papertrail_versions_job.rb | 53 ++++++++++++++ .../decidim/system/dashboard/show.html.erb | 12 ++++ config/i18n-tasks.yml | 2 + config/locales/en.yml | 10 +++ config/locales/fr.yml | 10 +++ config/secrets.yml | 4 ++ config/sidekiq.yml | 4 ++ lib/tasks/db.rake | 12 ++++ .../decidim/papertrail_versions_job_spec.rb | 69 +++++++++++++++++++ 11 files changed, 221 insertions(+) create mode 100644 app/controllers/decidim/system/dashboard_controller.rb create mode 100644 app/jobs/concerns/decidim/logging.rb create mode 100644 app/jobs/decidim/papertrail_versions_job.rb create mode 100644 app/views/decidim/system/dashboard/show.html.erb create mode 100644 spec/jobs/decidim/papertrail_versions_job_spec.rb diff --git a/app/controllers/decidim/system/dashboard_controller.rb b/app/controllers/decidim/system/dashboard_controller.rb new file mode 100644 index 0000000000..6ad9ba641d --- /dev/null +++ b/app/controllers/decidim/system/dashboard_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Decidim + module System + class DashboardController < Decidim::System::ApplicationController + before_action :check_organizations_presence + + def show + @organizations = Organization.all + @db_size = db_size + end + + def check_organizations_presence + return if Organization.exists? + + redirect_to new_organization_path + end + + private + + def db_size + dbname = ActiveRecord::Base.connection.current_database + sql = "SELECT pg_size_pretty(pg_database_size('#{dbname}'));" + ActiveRecord::Base.connection.execute(sql)[0]["pg_size_pretty"] + end + end + end +end diff --git a/app/jobs/concerns/decidim/logging.rb b/app/jobs/concerns/decidim/logging.rb new file mode 100644 index 0000000000..b4563270df --- /dev/null +++ b/app/jobs/concerns/decidim/logging.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Decidim + module Logging + private + + def log!(msg, level = :warn) + msg = "(#{self.class}) #{Time.current.strftime("%d-%m-%Y %H:%M")}> #{msg}" + case level + when :info + Rails.logger.info msg + else + Rails.logger.warn msg + end + end + end +end diff --git a/app/jobs/decidim/papertrail_versions_job.rb b/app/jobs/decidim/papertrail_versions_job.rb new file mode 100644 index 0000000000..d56427ab02 --- /dev/null +++ b/app/jobs/decidim/papertrail_versions_job.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Decidim + class PapertrailVersionsJob < ApplicationJob + queue_as :default + + include Decidim::Logging + + def perform(ret = nil) + ret = retention(ret) + + log! "Cleaning versions in database..." + log! "Cleaning item_types : #{item_types.join(", ")}" + + total = 0 + PaperTrail::Version.where(item_type: item_types).where("created_at <= ?", ret).in_batches(of: 5000) do |versions| + total += versions.size + versions.destroy_all + end + + log! "#{total} versions have been removed" + end + + private + + def retention(ret) + return ret if ret.present? && ret.is_a?(Time) + + ret = Rails.application.secrets.dig(:decidim, :database, :versions, :clean, :retention) + ret.months.ago + end + + # Exhaustive list of item_types to remove from versions table + def item_types + @item_types ||= %w( + Decidim::Accountability::TimelineEntry + Decidim::Accountability::Result + Decidim::Attachment + Decidim::AttachmentCollection + Decidim::Blogs::Post + Decidim::Budgets::Project + Decidim::Comments::Comment + Decidim::Conferences::MediaLink + Decidim::Conferences::Partner + Decidim::Debates::Debate + Decidim::Categorization + Decidim::Categorization + Decidim::Forms::Questionnaire + Decidim::UserBaseEntity + ) + end + end +end diff --git a/app/views/decidim/system/dashboard/show.html.erb b/app/views/decidim/system/dashboard/show.html.erb new file mode 100644 index 0000000000..5e5d3985c5 --- /dev/null +++ b/app/views/decidim/system/dashboard/show.html.erb @@ -0,0 +1,12 @@ +<% provide :title do %> +

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

+

<%= t ".current_organizations" %>

+ <%= render partial: "decidim/system/shared/organizations_list", locals: { organizations: @organizations } %> +<% end %> +
+

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

+
+

<%= I18n.t("decidim.system.titles.dashboard.info.db_size", db_size: @db_size) %>

+

<%= I18n.t("decidim.system.titles.dashboard.info.decidim_version", version: Decidim.version) %>

+
+
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index db9522009e..ee90a65c86 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -166,5 +166,7 @@ ignore_unused: - decidim.events.proposals.author_confirmation_proposal_event.email_intro - decidim.events.proposals.author_confirmation_proposal_event.email_outro - decidim.events.proposals.author_confirmation_proposal_event.notification_title + - decidim.system.titles.{dashboard,title} + - decidim.system.titles.info.* diff --git a/config/locales/en.yml b/config/locales/en.yml index 029ad64054..3b64f0604b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -210,6 +210,9 @@ en: please_sign_in: Please sign in sign_up: Sign up system: + dashboard: + show: + current_organizations: Current organizations organizations: omniauth_settings: france_connect: @@ -245,6 +248,13 @@ en: client_id: Client ID client_secret: Client secret site_url: Site URL + titles: + dashboard: + info: + db_size: 'Database size: %{db_size}' + decidim_version: 'Decidim version: v%{version}' + title: General informations + title: Dashboard verifications: authorizations: create: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index f50ecd0449..5093da2ba0 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -212,6 +212,9 @@ fr: please_sign_in: Veuillez vous connecter sign_up: Créer un compte system: + dashboard: + show: + current_organizations: Organisations organizations: omniauth_settings: france_connect: @@ -247,6 +250,13 @@ fr: client_id: Client ID client_secret: Client secret site_url: Site URL + titles: + dashboard: + info: + db_size: 'Poids base de données: %{db_size}' + decidim_version: 'Decidim version: v%{version}' + title: Informations générales + title: Tableau de bord système verifications: authorizations: create: diff --git a/config/secrets.yml b/config/secrets.yml index 78f94c3731..f85084b758 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -23,6 +23,10 @@ default: &default authorizations: export_data_to_userdata_enabled_for: <%= ENV.fetch("AUTO_EXPORT_AUTHORIZATIONS_DATA_TO_USER_DATA_ENABLED_FOR", "") %> currency: <%= ENV["CURRENCY"] || "€" %> + database: + versions: + clean: + retention: <%= ENV.fetch("DECIDIM_DB_VERSIONS_RETENTION", "6")&.to_i %> half_signup: show_tos_page_after_signup: <%= ENV.fetch("DECIDIM_HALF_SIGNUP_SHOW_TOS_PAGE_AFTER_SIGNUP", "true") == "true" %> initiatives: diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 06a488dcc0..fc2aeb03a0 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -79,3 +79,7 @@ cron: "0 9 0 * * *" class: Decidim::Cleaner::CleanDeletedUsersDataJob queue: scheduled + PapertrailCleanVersions: + cron: '0 0 3 * * *' + class: Decidim::PapertrailVersionJob + queue: default diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake index 60d590a509..82a8c09d8b 100644 --- a/lib/tasks/db.rake +++ b/lib/tasks/db.rake @@ -37,5 +37,17 @@ namespace :decidim do Decidim::SurveysService.new(verbose: true).clear end end + + namespace :versions do + desc "Clean versions" + task clean: :environment do + puts "(decidim:db:versions:clean) #{Time.current.strftime("%d-%m-%Y %H:%M:%S")}> Executing PapertrailVersionsJob..." + retention = Rails.application.secrets.dig(:decidim, :database, :versions, :clean, :retention) + retention = retention.months.ago + puts "(decidim:db:versions:clean) #{Time.current.strftime("%d-%m-%Y %H:%M:%S")}> Clean versions created before #{retention.strftime("%d-%m-%Y %H:%M:%S")}..." + Decidim::PapertrailVersionsJob.perform_later(retention) + puts "(decidim:db:versions:clean) #{Time.current.strftime("%d-%m-%Y %H:%M:%S")}> Job delayed to Sidekiq." + end + end end end diff --git a/spec/jobs/decidim/papertrail_versions_job_spec.rb b/spec/jobs/decidim/papertrail_versions_job_spec.rb new file mode 100644 index 0000000000..b3c98adf68 --- /dev/null +++ b/spec/jobs/decidim/papertrail_versions_job_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + describe PapertrailVersionsJob do + let!(:valid_versions) { create_list(:paper_trail_version, 10, item_id: 10) } + let!(:invalid_versions) { create_list(:paper_trail_version, 10, item_id: 10, item_type: item_type, created_at: created_at) } + let(:item_type) { "Decidim::UserBaseEntity" } + let(:created_at) { 8.months.ago } + + it "removes invalid versions" do + expect do + described_class.perform_now + end.to change(PaperTrail::Version, :count).from(20).to(10) + end + + it "allows to set the expiration limit" do + expect do + described_class.perform_now(11.months.ago) + end.not_to change(PaperTrail::Version, :count) + end + + context "when no versions older than 6 months" do + let(:created_at) { 1.day.ago } + + it "do not remove any papertrail version" do + expect do + described_class.perform_now + end.not_to change(PaperTrail::Version, :count) + end + end + + describe "#retention" do + subject { described_class.new.send(:retention, retention) } + let(:retention) { 8.months.ago } + + it "returns the defined retention" do + expect(subject).to eq(retention) + end + + context "when retention isn't a Date" do + let(:retention) { 8 } + + it "returns default value" do + expect(subject.strftime("%d-%m-%Y")).to eq(6.months.ago.strftime("%d-%m-%Y")) + end + end + + context "when retention is blank" do + let(:retention) { nil } + + it "returns default value" do + expect(subject.strftime("%d-%m-%Y")).to eq(6.months.ago.strftime("%d-%m-%Y")) + end + end + end + + describe "#item_types" do + subject { described_class.new.send(:item_types) } + + it "does not includes Proposals nor Initiatives" do + expect(subject).not_to include("Decidim::Proposals::Proposal") + expect(subject).not_to include("Decidim::Proposals::CollaborativeDraft") + expect(subject).not_to include("Decidim::Initiative") + end + end + end +end From 7e0f3b218bdecab20eedce12c75c13e5d1cb9143 Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:05:26 +0100 Subject: [PATCH 16/16] fix: Ensure coordinates from address field are not replaced (#648) Co-authored-by: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> --- config/initializers/extends.rb | 1 + .../geocoding/geocoder_coordinates_extends.rb | 16 ++ spec/lib/map/geocoding_spec.rb | 205 ++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 lib/extends/lib/decidim/geocoding/geocoder_coordinates_extends.rb create mode 100644 spec/lib/map/geocoding_spec.rb diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index 653d04fc73..451ff4a9d6 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -8,3 +8,4 @@ require "decidim/exporters/serializer" require "extends/lib/decidim/forms/user_answers_serializer_extend" +require "extends/lib/decidim/geocoding/geocoder_coordinates_extends" diff --git a/lib/extends/lib/decidim/geocoding/geocoder_coordinates_extends.rb b/lib/extends/lib/decidim/geocoding/geocoder_coordinates_extends.rb new file mode 100644 index 0000000000..1c44c7fe30 --- /dev/null +++ b/lib/extends/lib/decidim/geocoding/geocoder_coordinates_extends.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module GeocoderCoordinatesExtends + def coordinates(address, options = {}) + if address.to_s.match?(/^(-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)$/) + address_parts = address.to_s.split(/\s*,\s*/) + [address_parts[0].to_f, address_parts[1].to_f] + elsif (results = search(address, options)).size.positive? + results.first.coordinates + end + end +end + +Geocoder.singleton_class.class_eval do + prepend(GeocoderCoordinatesExtends) +end diff --git a/spec/lib/map/geocoding_spec.rb b/spec/lib/map/geocoding_spec.rb new file mode 100644 index 0000000000..a498f24a6f --- /dev/null +++ b/spec/lib/map/geocoding_spec.rb @@ -0,0 +1,205 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Map + module Provider + module Geocoding + # A dummy geocoding provider to test the geocoding utility. + class Test < ::Decidim::Map::Geocoding; end + end + end + + describe Geocoding do + include_context "with map utility" do + subject { utility } + + let(:utility_class) { Provider::Geocoding::Test } + end + + after do + Geocoder::Lookup::Test.reset + end + + describe "#initialize" do + let(:config) { { foo: "bar" } } + + it "configures Geocoder with the correct lookup configuration" do + expected_config = { + foo: "bar", + http_headers: { "Referer" => organization.host } + } + + expect(Geocoder).to receive(:configure).with( + test: expected_config + ) + expect(subject).to be_a(described_class) + expect(subject.configuration).to eq(expected_config) + end + end + + describe "#handle" do + it "returns the correct dummy provider's handle" do + expect(subject.handle).to eq(:test) + end + end + + describe "#search" do + let(:query) { double } + let(:options) { { foo: "bar" } } + + it "calls the Geocoder.search method with correct arguments" do + expect(Geocoder).to receive(:search).with( + query, + { lookup: :test, language: "en" }.merge(options) + ) + + subject.search(query, options) + end + end + + describe "#coordinates" do + let(:query) { double } + let(:options) { { foo: "bar" } } + + it "calls the Geocoder.coordinates method with correct arguments" do + expect(Geocoder).to receive(:coordinates).with( + query, + { lookup: :test, language: "en" }.merge(options) + ) + + subject.coordinates(query, options) + end + end + + describe "#address" do + let(:query) { double } + let(:options) { { foo: "bar" } } + + it "calls the Geocoder.search method with correct arguments" do + allow(Geocoder).to receive(:search).with( + query, + { lookup: :test, language: "en" }.merge(options) + ).and_return([]) + + subject.address(query, options) + end + end + + context "with geocoder stubs" do + let(:geocoder_search) { "New York, NY" } + let(:geocoder_results) do + [ + { + "coordinates" => [40.7143528, -74.0059731], + "address" => "New York, NY, USA", + "state" => "New York", + "state_code" => "NY", + "country" => "United States", + "country_code" => "US" + } + ] + end + + before do + Geocoder::Lookup::Test.add_stub(geocoder_search, geocoder_results) + end + + describe "#search" do + it "returns the geocoder stubbed results" do + results = subject.search(geocoder_search) + expect(results.length).to be(geocoder_results.length) + + geocoder_results.each_with_index do |result_values, ind| + result = results[ind] + result_values.each do |key, value| + expect(result.public_send(key)).to eq(value) + end + end + end + end + + describe "#coordinates" do + it "returns the geocoder stubbed first result" do + expect( + subject.coordinates(geocoder_search) + ).to eq([40.7143528, -74.0059731]) + end + + context "with coordinate strings" do + let(:geocoder_search) { "40.7143528, -74.0059731" } + + it "parses the string and returns the correct coordinates" do + expect(subject.coordinates("40.7143528, -74.0059731")).to eq([40.7143528, -74.0059731]) + expect( + subject.address(geocoder_search) + ).to eq("New York, NY, USA") + end + + it "raises an error if the coordinates are not valid" do + expect { subject.coordinates("40.7143528,, -74.0059731") }.to raise_error(ArgumentError) + expect { subject.coordinates("40,7143528, -74,0059731") }.to raise_error(ArgumentError) + end + end + end + + describe "#address" do + let(:geocoder_search) { [40.7143528, -74.0059731] } + + it "returns the geocoder stubbed first result" do + expect( + subject.address(geocoder_search) + ).to eq("New York, NY, USA") + end + + context "with multiple results" do + let(:geocoder_search) { [40.7143528, -74.0059731] } + let(:geocoder_results) do + [ + { + "coordinates" => [60.169857, 24.938379], + "address" => "Helsinki, Finland", + "state" => "Uusimaa", + "state_code" => "", + "country" => "Finland", + "country_code" => "FI" + }, + { + "coordinates" => [41.385063, 2.173404], + "address" => "Barcelona, Barcelona, Spain", + "state" => "Barcelona", + "state_code" => "", + "country" => "Spain", + "country_code" => "ES" + }, + { + "coordinates" => [40.7143520, -74.0059732], + "address" => "Closest Result, New York, NY, USA", + "state" => "New York", + "state_code" => "NY", + "country" => "United States", + "country_code" => "US" + }, + { + "coordinates" => [52.520008, 13.404954], + "address" => "Berlin, Berlin, Germany", + "state" => "Berlin", + "state_code" => "", + "country" => "Germany", + "country_code" => "DE" + } + ] + end + + it "returns the closest result" do + expect( + subject.address(geocoder_search) + ).to eq("Closest Result, New York, NY, USA") + end + end + end + end + end + end +end