From 0981952ade1e6ec23238b97b5b8cc6fb402f3abc Mon Sep 17 00:00:00 2001 From: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:33:22 +0100 Subject: [PATCH 01/13] fix: Rack attack can't be completely disabled (#449) * fix: backport from rake attack from cd44 * lint: Fix rubocop offenses * fix: System confirmation specs --- config/initializers/rack_attack.rb | 11 +++- lib/decidim_app/rack_attack.rb | 24 +++++++- spec/lib/decidim_app/rack_attack_spec.rb | 72 +++++++++++++++++------- spec/system/confirmation_spec.rb | 3 +- 4 files changed, 85 insertions(+), 25 deletions(-) diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 62de9c454c..559a117ab6 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -6,4 +6,13 @@ # Enabled by default in production # Can be deactivated with 'ENABLE_RACK_ATTACK=0' -DecidimApp::RackAttack.apply_configuration if DecidimApp::RackAttack.rack_enabled? +DecidimApp::RackAttack.deactivate_decidim_throttling! + +if DecidimApp::RackAttack.rack_enabled? + DecidimApp::RackAttack.enable_rack_attack! + DecidimApp::RackAttack.apply_configuration +else + DecidimApp::RackAttack.disable_rack_attack! +end + +DecidimApp::RackAttack.info! diff --git a/lib/decidim_app/rack_attack.rb b/lib/decidim_app/rack_attack.rb index da81cd82fa..00ebd86a07 100644 --- a/lib/decidim_app/rack_attack.rb +++ b/lib/decidim_app/rack_attack.rb @@ -4,21 +4,39 @@ module DecidimApp module RackAttack def self.rack_enabled? setting = Rails.application.secrets.dig(:decidim, :rack_attack, :enabled) - return setting == "1" if setting.present? + return setting.to_s == "1" if setting.present? Rails.env.production? end - def self.apply_configuration + def self.info! + Rails.logger.info("Rack::Attack is enabled: #{Rack::Attack.enabled}") + Rails.logger.info("Rack::Attack Fail2ban is enabled: #{DecidimApp::RackAttack::Fail2ban.enabled?}") + Rack::Attack.throttles.keys.each do |throttle| + Rails.logger.info("Rack::Attack throttling registered: #{throttle}") + end + end + + def self.enable_rack_attack! + Rails.logger.info("Rack::Attack is now enabled") Rack::Attack.enabled = true + end + def self.disable_rack_attack! + Rails.logger.info("Rack::Attack is now disabled") + Rack::Attack.enabled = false + end + + def self.deactivate_decidim_throttling! # Remove the original throttle from decidim-core - # see https://github.com/decidim/decidim/blob/release/0.26-stable/decidim-core/config/initializers/rack_attack.rb#L19 + # see https://github.com/decidim/decidim/blob/release/0.27-stable/decidim-core/config/initializers/rack_attack.rb#L19 DecidimApp::RackAttack::Throttling.deactivate_decidim_throttling! do Rails.logger.info("Deactivating 'requests by ip' from Decidim Core") Rack::Attack.throttles.delete("requests by ip") end + end + def self.apply_configuration Rack::Attack.throttled_response_retry_after_header = true Rack::Attack.throttled_responder = lambda do |request| diff --git a/spec/lib/decidim_app/rack_attack_spec.rb b/spec/lib/decidim_app/rack_attack_spec.rb index 0aca106378..608a7ce3c2 100644 --- a/spec/lib/decidim_app/rack_attack_spec.rb +++ b/spec/lib/decidim_app/rack_attack_spec.rb @@ -62,40 +62,66 @@ end end - describe "#apply_configuration" do + describe "#enable_rack_attack!" do + before do + described_class.enable_rack_attack! + end + + it "enables Rack::Attack" do + expect(Rack::Attack.enabled).to be_truthy + end + end + + describe "#disable_rack_attack!" do + before do + described_class.disable_rack_attack! + end + + it "enables Rack::Attack" do + expect(Rack::Attack.enabled).to be_falsey + end + end + + describe "#deactivate_decidim_throttling!" do before do - described_class.apply_configuration - Rack::Attack.reset! + described_class.deactivate_decidim_throttling! end + it "deactivates Decidim throttling" do + # Decidim throttling is deactivated by default in rails env test + # https://github.com/decidim/decidim/blob/release/0.27-stable/decidim-core/config/initializers/rack_attack.rb#L19 + expect(Rack::Attack.throttles.keys.join).to include("limit confirmations attempts per code") + end + end + + describe "#apply_configuration" do describe "Throttling" do let(:headers) { { "REMOTE_ADDR" => "1.2.3.4", "decidim.current_organization" => organization } } + let(:rack_max_requests) { 15 } - it "successful for 100 requests, then blocks the user" do - 100.times do - get decidim.root_path, params: {}, headers: headers - expect(response).to have_http_status(:ok) - end - - get decidim.root_path, params: {}, headers: headers - expect(response).to have_http_status(:too_many_requests) - expect(response.body).to include("Your connection has been slowed because server received too many requests.") + before do + allow(Rails.application.secrets).to receive(:dig).with(any_args).and_call_original + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :rack_attack, :throttle, :max_requests).and_return(rack_max_requests) + described_class.apply_configuration + Rack::Attack.reset! + described_class.enable_rack_attack! + end - travel_to(1.minute.from_now) do - get decidim.root_path, params: {}, headers: headers - expect(response).to have_http_status(:ok) - end + it "defines default period and max_requests" do + expect(DecidimApp::RackAttack::Throttling.max_requests).to eq(rack_max_requests) + expect(DecidimApp::RackAttack::Throttling.period).to eq(60) end - it "successful for 99 requests" do - 99.times do + it "successful for 15 requests, then blocks the user" do + rack_max_requests.times do get decidim.root_path, params: {}, headers: headers expect(response).to have_http_status(:ok) + expect(response.body).not_to include("Your connection has been slowed because server received too many requests.") end get decidim.root_path, params: {}, headers: headers - expect(response.body).not_to include("Your connection has been slowed because server received too many requests.") - expect(response).not_to have_http_status(:too_many_requests) + expect(response).to have_http_status(:too_many_requests) + expect(response.body).to include("Your connection has been slowed because server received too many requests.") travel_to(1.minute.from_now) do get decidim.root_path, params: {}, headers: headers @@ -107,6 +133,12 @@ describe "Fail2Ban" do let(:headers) { { "REMOTE_ADDR" => "1.2.3.4", "decidim.current_organization" => organization } } + before do + described_class.apply_configuration + Rack::Attack.reset! + described_class.enable_rack_attack! + end + %w(/etc/passwd /wp-admin/index.php /wp-login/index.php SELECT CONCAT /.git/config).each do |path| it "blocks user for specific request : '#{path}'" do get "#{decidim.root_path}#{path}", params: {}, headers: headers diff --git a/spec/system/confirmation_spec.rb b/spec/system/confirmation_spec.rb index 3542bbbadb..b1294c5d46 100644 --- a/spec/system/confirmation_spec.rb +++ b/spec/system/confirmation_spec.rb @@ -76,6 +76,7 @@ def code_for(str) before do allow(Rails).to receive(:cache).and_return(memory_store) + DecidimApp::RackAttack.enable_rack_attack! DecidimApp::RackAttack.apply_configuration Rack::Attack.reset! @@ -88,7 +89,7 @@ def code_for(str) end after do - Rack::Attack.enabled = false + DecidimApp::RackAttack.disable_rack_attack! end it "throttles after 5 attempts per minute" do From 0d0d01ea15bd0070747b60bbd3c48397de9624eb Mon Sep 17 00:00:00 2001 From: quentinchampenois <26109239+Quentinchampenois@users.noreply.github.com> Date: Thu, 9 Nov 2023 10:57:43 +0100 Subject: [PATCH 02/13] fix: Devise emails --- Gemfile | 2 +- Gemfile.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index 12f44e8e8f..a433aa1186 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,7 @@ gem "decidim-homepage_interactive_map", git: "https://github.com/OpenSourcePolit gem "decidim-ludens", git: "https://github.com/OpenSourcePolitics/decidim-ludens.git", branch: DECIDIM_BRANCH gem "decidim-phone_authorization_handler", git: "https://github.com/OpenSourcePolitics/decidim-module_phone_authorization_handler", branch: DECIDIM_BRANCH gem "decidim-spam_detection" -gem "decidim-term_customizer", git: "https://github.com/armandfardeau/decidim-module-term_customizer.git", branch: "fix/precompile-on-docker" +gem "decidim-term_customizer", git: "https://github.com/OpenSourcePolitics/decidim-module-term_customizer.git", branch: "fix/email_with_precompile" # Omniauth gems gem "omniauth-france_connect", git: "https://github.com/OpenSourcePolitics/omniauth-france_connect" diff --git a/Gemfile.lock b/Gemfile.lock index c056175901..3acc543f5a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,6 +33,15 @@ GIT rgeo (~> 2.4) rgeo-proj4 (~> 3.1) +GIT + remote: https://github.com/OpenSourcePolitics/decidim-module-term_customizer.git + revision: 54050b317815bff1edaad3d26744a941be1d31bf + branch: fix/email_with_precompile + specs: + decidim-term_customizer (0.27.0) + decidim-admin (~> 0.27.0) + decidim-core (~> 0.27.0) + GIT remote: https://github.com/OpenSourcePolitics/decidim-module_phone_authorization_handler revision: 79f2a5f6c3357d63f92423a2b173893f4c4d06d8 @@ -66,15 +75,6 @@ GIT decidim-core (>= 0.27.0, < 0.28) deface (~> 1.5) -GIT - remote: https://github.com/armandfardeau/decidim-module-term_customizer.git - revision: 5c2b648e07c5fd51e2598256886895b9a83d698c - branch: fix/precompile-on-docker - specs: - decidim-term_customizer (0.27.0) - decidim-admin (~> 0.27.0) - decidim-core (~> 0.27.0) - GIT remote: https://github.com/sgruhier/foundation_rails_helper.git revision: bc33600db7a2d16ce3cdc1f8369d0d7e7c4245b5 From 3c9e7ee408323e961f6556471e766351481c5a5c Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:21:22 +0100 Subject: [PATCH 03/13] fix: downgrade wicked_pdf (#458) * fix: downgrade wicked_pdf * fix: Add the requirement of the wicked_pdf --- Gemfile.lock | 2 +- config/application.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3acc543f5a..d4b4f59ae9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1043,7 +1043,7 @@ GEM websocket-extensions (0.1.5) wicked (1.4.0) railties (>= 3.0.7) - wicked_pdf (2.7.0) + wicked_pdf (2.6.3) activesupport wisper (2.0.1) wisper-rspec (1.1.0) diff --git a/config/application.rb b/config/application.rb index 03ab7b9a20..0872b7afb3 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,8 +9,7 @@ # require "action_text/engine" require_relative "../lib/active_storage/downloadable" -# TODO : add missing dep to decidim-initiatives/lib/decidim/initiatives/engine.rb -# require "wicked_pdf" +require "wicked_pdf" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. From 8965d5e28508ed76d3da87c9fc92797a3ccfbb89 Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:33:57 +0100 Subject: [PATCH 04/13] fix: add missing locales (#457) * fix: add missing locales * fix: Add new locales to ignored unused ones --- config/i18n-tasks.yml | 1 + config/locales/en.yml | 6 ++++++ config/locales/fr.yml | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 5c74a222e5..befbb25417 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -127,3 +127,4 @@ ignore_unused: - decidim.proposals.collaborative_drafts.new.* - decidim.admin.menu.admin_accountability - decidim.anonymous_user + - decidim.initiatives.pages.home.highlighted_initiatives.* diff --git a/config/locales/en.yml b/config/locales/en.yml index a09fee63c0..25bdb8f15d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -60,6 +60,12 @@ en: email_outro: Check the Verifications's conflicts list and contact the participant to verify their details and solve the issue. email_subject: Failed verification attempt against a managed participant notification_title: The participant %{resource_title} has tried to verify themself with the data of the managed participant %{managed_user_name}. + initiatives: + pages: + home: + highlighted_initiatives: + active_initiatives: Active initiatives + see_all_initiatives: See all initiatives meetings: directory: meetings: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 0f572dad32..92b7b6c56e 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -62,6 +62,12 @@ fr: email_outro: Vérifiez la Liste de conflits des vérifications et contactez les participants pour vérifier leurs renseignements et résoudre le problème. email_subject: Un utilisateur a tenté de se faire vérifier avec les données d'un utilisateur représenté notification_title: Le participant %{resource_title} a tenté de se faire vérifier avec les données de l'utilisateur représenté %{managed_user_name}. + initiatives: + pages: + home: + highlighted_initiatives: + active_initiatives: Pétitions actives + see_all_initiatives: Voir toutes les pétitions meetings: directory: meetings: From 637cd4788b934dfe88d5ab721d78bf5e184d4629 Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:44:00 +0100 Subject: [PATCH 05/13] fix: change the commit to fix notifications not sending on dcd-app (#463) --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d4b4f59ae9..17df2f5aad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -35,7 +35,7 @@ GIT GIT remote: https://github.com/OpenSourcePolitics/decidim-module-term_customizer.git - revision: 54050b317815bff1edaad3d26744a941be1d31bf + revision: bfb4ba25dcfe504c9c9d7afd376c04c28cb23ce8 branch: fix/email_with_precompile specs: decidim-term_customizer (0.27.0) From a1517c21fc493fdefa6650a2c110ec659e8b9967 Mon Sep 17 00:00:00 2001 From: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:39:59 +0100 Subject: [PATCH 06/13] feat: Configure Puma server and Local docker-compose with HTTPS (#445) * feat: Local docker-compose * fix: Update make run command * fix: Local docker-compose * fix: Webpacker compilation * fix: Local docker-compose * fix: Docker-compose with https in local * doc: Add Getting started with Docker in README * fix: Add letter_opener gem to global scope * feat: Add docker commands to Makefile * fix: backport fix docker-compose * refactor: Remove not needed entrypoint.local.sh * feat: Generate SSL certificate in Dockerfile.local * fix: Fine tuning puma on docker-compose local * lint: Fix rubocop offense * Update GETTING_STARTED_DOCKER.md fix typo --------- Co-authored-by: ailepet --- .env-example | 7 ++- .gitignore | 1 + Dockerfile.local | 56 +++++++++++++++++++ Gemfile | 2 +- Makefile | 51 ++++++++--------- README.md | 1 + config/puma.rb | 14 +++-- ...ompose.dev.yml => docker-compose.local.yml | 44 +++++++++++---- docs/GETTING_STARTED_DOCKER.md | 35 ++++++++++++ 9 files changed, 168 insertions(+), 43 deletions(-) create mode 100644 Dockerfile.local rename docker-compose.dev.yml => docker-compose.local.yml (56%) create mode 100644 docs/GETTING_STARTED_DOCKER.md diff --git a/.env-example b/.env-example index cc4ee09c86..42e2541580 100644 --- a/.env-example +++ b/.env-example @@ -67,4 +67,9 @@ DEFACE_ENABLED=false DECIDIM_ADMIN_PASSWORD_EXPIRATION_DAYS=365 DECIDIM_ADMIN_PASSWORD_MIN_LENGTH=15 DECIDIM_ADMIN_PASSWORD_REPETITION_TIMES=5 -DECIDIM_ADMIN_PASSWORD_STRONG="false" \ No newline at end of file +DECIDIM_ADMIN_PASSWORD_STRONG="false" +# Puma server configuration +# PUMA_MIN_THREADS=5 +# PUMA_MAX_THREADS=5 +# PUMA_WORKERS=0 +# PUMA_PRELOAD_APP=false \ No newline at end of file diff --git a/.gitignore b/.gitignore index 95159cb151..48b37409bf 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,4 @@ yarn-debug.log* coverage/ public/sw.js* app/compiled_views/ +certificate-https-local/ diff --git a/Dockerfile.local b/Dockerfile.local new file mode 100644 index 0000000000..979fea4a97 --- /dev/null +++ b/Dockerfile.local @@ -0,0 +1,56 @@ +# Builder Stage +FROM ruby:3.0.6-slim as builder + +ENV RAILS_ENV=production \ + SECRET_KEY_BASE=dummy + +WORKDIR /app + +RUN apt-get update -q && \ + apt-get install -yq libpq-dev curl git libicu-dev build-essential openssl && \ + curl https://deb.nodesource.com/setup_16.x | bash && \ + apt-get install -y nodejs && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + npm install --global yarn && \ + gem install bundler:2.4.9 + +COPY Gemfile Gemfile.lock ./ +RUN bundle config set --local without 'development test' && \ + bundle install -j"$(nproc)" + +COPY package.json yarn.lock ./ +COPY packages packages +RUN yarn install --frozen-lock + +COPY . . + +RUN bundle exec bootsnap precompile --gemfile app/ lib/ config/ bin/ db/ && \ + bundle exec rails deface:precompile && \ + bundle exec rails assets:precompile + +run mkdir certificate-https-local +RUN openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=FR/ST=France/L=Paris/O=decidim/CN=decidim.eu" -keyout ./certificate-https-local/key.pem -out ./certificate-https-local/cert.pem; + +# Runner Stage +FROM ruby:3.0.6-slim as runner + +ENV RAILS_ENV=production \ + SECRET_KEY_BASE=dummy \ + RAILS_LOG_TO_STDOUT=true \ + LD_PRELOAD="libjemalloc.so.2" \ + MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:5000,muzzy_decay_ms:5000,narenas:2" + +WORKDIR /app + +RUN apt-get update -q && \ + apt-get install -yq postgresql-client imagemagick libproj-dev proj-bin libjemalloc2 && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + gem install bundler:2.4.9 + +COPY --from=builder /usr/local/bundle /usr/local/bundle +COPY --from=builder /app /app + +EXPOSE 3000 +CMD ["bundle", "exec", "rails", "server", "-b", "ssl://0.0.0.0:3000?key=/app/certificate-https-local/key.pem&cert=/app/certificate-https-local/cert.pem"] \ No newline at end of file diff --git a/Gemfile b/Gemfile index a433aa1186..f0868d5674 100644 --- a/Gemfile +++ b/Gemfile @@ -42,6 +42,7 @@ gem "dotenv-rails", "~> 2.7" gem "faker", "~> 2.14" gem "fog-aws" gem "foundation_rails_helper", git: "https://github.com/sgruhier/foundation_rails_helper.git" +gem "letter_opener_web", "~> 1.3" gem "nokogiri", "1.13.4" gem "omniauth-rails_csrf_protection", "~> 1.0" gem "puma", ">= 5.5.1" @@ -49,7 +50,6 @@ gem "rack-attack", "~> 6.6" gem "sys-filesystem" group :development do - gem "letter_opener_web", "~> 1.3" gem "listen", "~> 3.1" gem "rubocop-faker" gem "spring", "~> 2.0" diff --git a/Makefile b/Makefile index cb66c86f1c..36fc450e34 100644 --- a/Makefile +++ b/Makefile @@ -1,35 +1,36 @@ -# Starts with production configuration -local-prod: - docker-compose up -d - -# Starts with development configuration -# TODO: Fix seeds for local-dev make command -local-dev: - docker-compose -f docker-compose.dev.yml up -d - @make create-database - @make run-migrations - #@make create-seeds +run: up + @make create-seeds + +up: + docker-compose -f docker-compose.local.yml up --build -d + @make setup-database # Stops containers and remove volumes teardown: - docker-compose down -v --rmi all - -# Starts containers and restore dump -local-restore: - @make create-database - @make -i restore-dump - @make run-migrations - @make start + docker-compose -f docker-compose.local.yml down -v --rmi all -# Create database create-database: - docker-compose run app bundle exec rails db:create -# Run migrations -run-migrations: - docker-compose run app bundle exec rails db:migrate + docker-compose -f docker-compose.local.yml exec app /bin/bash -c 'DISABLE_DATABASE_ENVIRONMENT_CHECK=1 /usr/local/bundle/bin/bundle exec rake db:create' + +setup-database: create-database + docker-compose -f docker-compose.local.yml exec app /bin/bash -c 'DISABLE_DATABASE_ENVIRONMENT_CHECK=1 /usr/local/bundle/bin/bundle exec rake db:migrate' + # Create seeds create-seeds: - docker-compose exec -e RAILS_ENV=development app /bin/bash -c '/usr/local/bundle/bin/bundle exec rake db:seed' + docker-compose -f docker-compose.local.yml exec app /bin/bash -c 'DISABLE_DATABASE_ENVIRONMENT_CHECK=1 /usr/local/bundle/bin/bundle exec rake db:schema:load db:seed' + # Restore dump restore-dump: bundle exec rake restore_dump + +shell: + docker-compose -f docker-compose.local.yml exec app /bin/bash + +restart: + docker-compose -f docker-compose.local.yml up -d + +status: + docker-compose -f docker-compose.local.yml ps + +logs: + docker-compose -f docker-compose.local.yml logs app \ No newline at end of file diff --git a/README.md b/README.md index 6d06bc9be4..0494b2739c 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Some non-official customizations can be found see [OVERLOADS.MD](./OVERLOADS.md) ## 🚀 Getting started - See our [installation guide](./docs/GETTING_STARTED.md) to run a decidim-app by OSP locally +- See our [Docker installation guide](./docs/GETTING_STARTED_DOCKER.md) to run a decidim-app by OSP locally with Docker - See our [homepage interactive map module](./docs/HOMEPAGE_INTERACTIVE_MAP.md) to configure module (OSX/Ubuntu) ## 👋 Contributing diff --git a/config/puma.rb b/config/puma.rb index a8adec5d17..0e53bb5f12 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -5,9 +5,10 @@ # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. -# -threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) -threads threads_count, threads_count + +min_threads_count = ENV.fetch("PUMA_MIN_THREADS", 5).to_i +max_threads_count = ENV.fetch("PUMA_MAX_THREADS", 5).to_i +threads min_threads_count, max_threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # @@ -22,15 +23,16 @@ # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). -# -# workers ENV.fetch("WEB_CONCURRENCY") { 2 } +workers_count = ENV.fetch("PUMA_WORKERS", -1).to_i +workers workers_count if workers_count.positive? # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write # process behavior so workers use less memory. # -# preload_app! + +preload_app! if ENV.fetch("PUMA_PRELOAD_APP", "false") == "true" # Allow puma to be restarted by `rails restart` command. plugin :tmp_restart diff --git a/docker-compose.dev.yml b/docker-compose.local.yml similarity index 56% rename from docker-compose.dev.yml rename to docker-compose.local.yml index 6f6376a03f..65ee35b4a4 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.local.yml @@ -19,35 +19,59 @@ services: sidekiq: build: context: . + dockerfile: Dockerfile.local command: [ "bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml" ] environment: - - REDIS_URL=redis://redis:6379 - - MEMCACHE_SERVERS=memcached:11211 - DATABASE_HOST=database - DATABASE_USERNAME=postgres + - DECIDIM_HOST=localhost + - REDIS_URL=redis://redis:6379 + - MEMCACHE_SERVERS=memcached:11211 + - RAILS_SERVE_STATIC_FILES=true + - RAILS_LOG_TO_STDOUT=true + - ASSET_HOST=localhost:3000 + - FORCE_SSL=1 + - ENABLE_LETTER_OPENER=1 + - SEED=true + - DEFACE_ENABLED=false + - QUESTION_CAPTCHA_HOST= + - ENABLE_RACK_ATTACK=0 + - PUMA_MIN_THREADS=5 + - PUMA_MAX_THREADS=5 + - PUMA_WORKERS=4 + - PUMA_PRELOAD_APP=true depends_on: - app + volumes: + - shared-volume:/app links: - database - redis app: build: context: . - volumes: - - .:/app - - node_modules:/app/node_modules + dockerfile: Dockerfile.local environment: - DATABASE_HOST=database - DATABASE_USERNAME=postgres - - DECIDIM_HOST=0.0.0.0 + - DECIDIM_HOST=localhost - REDIS_URL=redis://redis:6379 - MEMCACHE_SERVERS=memcached:11211 - RAILS_SERVE_STATIC_FILES=true - RAILS_LOG_TO_STDOUT=true - - FORCE_SSL="0" - - LETTER_OPENER_ENABLED="true" + - ASSET_HOST=localhost:3000 + - FORCE_SSL=1 + - ENABLE_LETTER_OPENER=1 - SEED=true - DEFACE_ENABLED=false + - QUESTION_CAPTCHA_HOST= + - ENABLE_RACK_ATTACK=0 + - PUMA_MIN_THREADS=5 + - PUMA_MAX_THREADS=5 + - PUMA_WORKERS=4 + - PUMA_PRELOAD_APP=true + volumes: + - shared-volume:/app ports: - 3000:3000 depends_on: @@ -56,6 +80,6 @@ services: - memcached volumes: - node_modules: { } + shared-volume: { } pg-data: { } - redis-data: { } + redis-data: { } \ No newline at end of file diff --git a/docs/GETTING_STARTED_DOCKER.md b/docs/GETTING_STARTED_DOCKER.md new file mode 100644 index 0000000000..40b64d1521 --- /dev/null +++ b/docs/GETTING_STARTED_DOCKER.md @@ -0,0 +1,35 @@ +# Starting DecidimApp on Docker with HTTPS ! + +## Requirements +* **Docker** +* **Docker-compose** +* **Git** +* **Make** +* **OpenSSL** +* **PostgreSQL** 14+ + +## Installation + +### Setup a clean Decidim App + +1. Clone repository +2. Create a `.env` file from `.env.example` and fill it with your own values +3. Start the application with `make up` + +Once containers are deployed, you should be able to visit : https://localhost:3000 + +Also, you should be automatically redirected to https://localhost:3000/system because your database is empty. + +### Setup a seeded DecidimApp + +1. Clone repository +2. Create a `.env` file from `.env-example` and fill it with your own values +3. Start the application with `make run` + +Once containers are deployed, you should be able to visit : https://localhost:3000/ without being redirected ! + +## Informations + +* Please use the `docker-compose.local.yml` in local environment because it uses `Dockerfile.local` which includes self signed certificate and allows to enable https in localhost +* If you want to cleanup your environmen run `make teardown` : it will stop containers and remove volumes and images + From 2e4c42c7c046d012f0ae59b714e7ee9b3e193d2e Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:59:37 +0100 Subject: [PATCH 07/13] backport: Answer Initiatives Notifications (#464) * backport: Add initiative answers notifications * lint: Rubocop fixes * fix: Add some tests and fix i18n * fix: Delete factory_bot_spec as we didn't do any customization of the factories * fix: Add new tests --- .../initiatives/answer_initiative_event.rb | 8 ++ config/application.rb | 2 + config/i18n-tasks.yml | 2 + config/locales/en.yml | 6 + config/locales/fr.yml | 6 + .../admin/update_initiative_answer_extends.rb | 56 ++++++++ .../helpers/decidim/icon_helper_extends.rb | 23 +++ .../admin/update_initiative_answer_spec.rb | 80 +++++++++++ spec/factories.rb | 1 + spec/factory_bot_spec.rb | 9 -- spec/helpers/icon_helper_spec.rb | 131 ++++++++++++++++++ .../update_initiative_answer_example.rb | 85 ++++++++++++ 12 files changed, 400 insertions(+), 9 deletions(-) create mode 100644 app/events/decidim/initiatives/answer_initiative_event.rb create mode 100644 lib/extends/commands/decidim/initiatives/admin/update_initiative_answer_extends.rb create mode 100644 lib/extends/helpers/decidim/icon_helper_extends.rb create mode 100644 spec/commands/decidim/initiatives/admin/update_initiative_answer_spec.rb delete mode 100644 spec/factory_bot_spec.rb create mode 100644 spec/helpers/icon_helper_spec.rb create mode 100644 spec/shared/update_initiative_answer_example.rb diff --git a/app/events/decidim/initiatives/answer_initiative_event.rb b/app/events/decidim/initiatives/answer_initiative_event.rb new file mode 100644 index 0000000000..69940dec2e --- /dev/null +++ b/app/events/decidim/initiatives/answer_initiative_event.rb @@ -0,0 +1,8 @@ +# frozen-string_literal: true + +module Decidim + module Initiatives + class AnswerInitiativeEvent < Decidim::Events::SimpleEvent + end + end +end diff --git a/config/application.rb b/config/application.rb index 0872b7afb3..ac2b3c6f06 100644 --- a/config/application.rb +++ b/config/application.rb @@ -47,6 +47,8 @@ class Application < Rails::Application require "extends/controllers/decidim/devise/sessions_controller_extends" require "extends/controllers/decidim/editor_images_controller_extends" require "extends/services/decidim/iframe_disabler_extends" + require "extends/helpers/decidim/icon_helper_extends" + require "extends/commands/decidim/initiatives/admin/update_initiative_answer_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/i18n-tasks.yml b/config/i18n-tasks.yml index befbb25417..89d9ee47b4 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -127,4 +127,6 @@ ignore_unused: - decidim.proposals.collaborative_drafts.new.* - decidim.admin.menu.admin_accountability - decidim.anonymous_user + - decidim.events.initiatives.initiative_answered.* - decidim.initiatives.pages.home.highlighted_initiatives.* + diff --git a/config/locales/en.yml b/config/locales/en.yml index 25bdb8f15d..543840c3ce 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -48,6 +48,12 @@ en: email_outro: You have received this notification because you are participating in "%{participatory_space_title}" email_subject: Your vote is still pending in %{participatory_space_title} notification_title: The vote on budget %{resource_title} is still waiting for your confirmation in %{participatory_space_title} + initiatives: + initiative_answered: + email_intro: The initiative "%{resource_title}" has been answered. + email_outro: You have received this notification because you are following the initiative "%{resource_title}". + email_subject: Initiative "%{resource_title}" has been answered + notification_title: The initiative %{resource_title} has been answered. users: user_officialized: email_intro: Participant %{name} (%{nickname}) has been officialized. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 92b7b6c56e..93f277f432 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -50,6 +50,12 @@ fr: email_outro: Vous avez reçu cette notification parce que vous avez commencé à voter sur la concertation "%{participatory_space_title}" email_subject: Votre vote est toujours en attente sur la concertation %{participatory_space_title} notification_title: Votre vote pour le budget %{resource_title} attend d'être finalisé sur la concertation %{participatory_space_title} + initiatives: + initiative_answered: + email_intro: La pétition "%{resource_title}" a reçu une réponse. + email_outro: Vous avez reçu cette notification parce que vous suivez la pétition "%{resource_title}". + email_subject: La pétition "%{resource_title}" a reçu une réponse. + notification_title: La pétition %{resource_title} a reçu une réponse. users: user_officialized: email_intro: Le participant %{name} (%{nickname}) a été officialisé. diff --git a/lib/extends/commands/decidim/initiatives/admin/update_initiative_answer_extends.rb b/lib/extends/commands/decidim/initiatives/admin/update_initiative_answer_extends.rb new file mode 100644 index 0000000000..aced25d9b7 --- /dev/null +++ b/lib/extends/commands/decidim/initiatives/admin/update_initiative_answer_extends.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module UpdateInitiativeAnswerExtends + def call + return broadcast(:invalid) if form.invalid? + + @initiative = Decidim.traceability.update!( + initiative, + current_user, + attributes + ) + notify_initiative_is_extended if @notify_extended + notify_initiative_is_answered if @notify_answered + broadcast(:ok, initiative) + rescue ActiveRecord::RecordInvalid + broadcast(:invalid, initiative) + end + + private + + def attributes + attrs = { + answer: form.answer, + answer_url: form.answer_url + } + + attrs[:answered_at] = Time.current if form.answer.present? + + if form.signature_dates_required? + attrs[:signature_start_date] = form.signature_start_date + attrs[:signature_end_date] = form.signature_end_date + + if initiative.published? && form.signature_end_date != initiative.signature_end_date && + form.signature_end_date > initiative.signature_end_date + @notify_extended = true + end + end + + @notify_answered = form.answer != initiative.answer && !form.answer.values.all?(&:blank?) + + attrs + end + + def notify_initiative_is_answered + Decidim::EventsManager.publish( + event: "decidim.events.initiatives.initiative_answered", + event_class: Decidim::Initiatives::AnswerInitiativeEvent, + resource: initiative, + followers: initiative.followers + ) + end +end + +Decidim::Initiatives::Admin::UpdateInitiativeAnswer.class_eval do + prepend UpdateInitiativeAnswerExtends +end diff --git a/lib/extends/helpers/decidim/icon_helper_extends.rb b/lib/extends/helpers/decidim/icon_helper_extends.rb new file mode 100644 index 0000000000..b66490303c --- /dev/null +++ b/lib/extends/helpers/decidim/icon_helper_extends.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module IconHelperExtends + def resource_icon(resource, options = {}) + if resource.instance_of?(Decidim::Initiative) + icon "initiatives", options + elsif resource.instance_of?(Decidim::Comments::Comment) + icon "comment-square", options + elsif resource.respond_to?(:component) && resource.component + component_icon(resource.component, options) + elsif resource.respond_to?(:manifest) && resource.manifest + manifest_icon(resource.manifest, options) + elsif resource.is_a?(Decidim::User) + icon "person", options + else + icon "bell", options + end + end +end + +Decidim::IconHelper.module_eval do + prepend(IconHelperExtends) +end diff --git a/spec/commands/decidim/initiatives/admin/update_initiative_answer_spec.rb b/spec/commands/decidim/initiatives/admin/update_initiative_answer_spec.rb new file mode 100644 index 0000000000..81f87c8820 --- /dev/null +++ b/spec/commands/decidim/initiatives/admin/update_initiative_answer_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Initiatives + module Admin + describe UpdateInitiativeAnswer do + let(:form_klass) { Decidim::Initiatives::Admin::InitiativeAnswerForm } + + context "when valid data" do + it_behaves_like "update an initiative answer" do + context "when the user is an admin" do + let!(:current_user) { create(:user, :admin, organization: initiative.organization) } + let!(:follower) { create(:user, organization: organization) } + let!(:follow) { create(:follow, followable: initiative, user: follower) } + + it "notifies the followers for extension and answer" do + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.initiatives.initiative_extended", + event_class: Decidim::Initiatives::ExtendInitiativeEvent, + resource: initiative, + followers: [follower] + ) + .ordered + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.initiatives.initiative_answered", + event_class: Decidim::Initiatives::AnswerInitiativeEvent, + resource: initiative, + followers: [follower] + ) + .ordered + + command.call + end + + context "when the signature end time is not modified" do + let(:signature_end_date) { initiative.signature_end_date } + + it "doesn't notify the followers" do + expect(Decidim::EventsManager).not_to receive(:publish).with( + event: "decidim.events.initiatives.initiative_extended", + event_class: Decidim::Initiatives::ExtendInitiativeEvent, + resource: initiative, + followers: [follower] + ) + + command.call + end + end + end + end + end + + context "when validation failure" do + let(:organization) { create(:organization) } + let!(:initiative) { create(:initiative, organization: organization) } + let!(:form) do + form_klass + .from_model(initiative) + .with_context(current_organization: organization, initiative: initiative) + end + + let(:command) { described_class.new(initiative, form, initiative.author) } + + it "broadcasts invalid" do + expect(initiative).to receive(:valid?) + .at_least(:once) + .and_return(false) + expect { command.call }.to broadcast :invalid + end + end + end + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 16258e4b03..3a7bdad7e9 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -12,3 +12,4 @@ require "decidim/verifications/test/factories" require "decidim/forms/test/factories" require "decidim/surveys/test/factories" +require "decidim/initiatives/test/factories" diff --git a/spec/factory_bot_spec.rb b/spec/factory_bot_spec.rb deleted file mode 100644 index d478a75b7f..0000000000 --- a/spec/factory_bot_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe FactoryBot, processing_uploads_for: Decidim::AttachmentUploader do - it "has 100% valid factories" do - expect { described_class.lint(traits: true) }.not_to raise_error - end -end diff --git a/spec/helpers/icon_helper_spec.rb b/spec/helpers/icon_helper_spec.rb new file mode 100644 index 0000000000..6596b11947 --- /dev/null +++ b/spec/helpers/icon_helper_spec.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + describe IconHelper do + describe "#component_icon" do + let(:component) do + create(:component, manifest_name: :dummy) + end + + describe "when the component has no icon" do + before do + allow(component.manifest).to receive(:icon).and_return(nil) + end + + it "returns a fallback" do + result = helper.component_icon(component) + expect(result).to include("question-mark") + end + end + + describe "when the component has icon" do + it "returns the icon" do + result = helper.component_icon(component) + expect(result).to eq <<~SVG.strip + + SVG + end + + context "with role attribute specified" do + it "implements role attribute" do + result = helper.component_icon(component, role: "img") + expect(result).to eq <<~SVG.strip + + SVG + end + end + + context "with no role attribute specified" do + it "doesn't implement role attribute" do + result = helper.component_icon(component) + expect(result).to eq <<~SVG.strip + + SVG + end + end + end + + describe "resource_icon" do + let(:result) { helper.resource_icon(resource) } + + context "when it has a component" do + let(:resource) { build :dummy_resource } + + it "renders the component icon" do + expect(helper).to receive(:component_icon).with(resource.component, {}) + + result + end + end + + context "when it has a manifest" do + let(:resource) { build(:component, manifest_name: :dummy) } + + it "renders the manifest icon" do + expect(helper).to receive(:manifest_icon).with(resource.manifest, {}) + + result + end + end + + context "when it is a user" do + let(:resource) { build :user } + + it "renders a person icon" do + expect(result).to include("svg#icon-person") + end + end + + context "when the resource component and manifest are nil" do + let(:resource) { build :dummy_resource } + + before do + allow(resource).to receive(:component).and_return(nil) + end + + it "renders a generic icon" do + expect(result).to include("svg#icon-bell") + end + end + + context "when the manifest icon is nil" do + let(:resource) { build(:component, manifest_name: :dummy) } + + before do + allow(resource.manifest).to receive(:icon).and_return(nil) + end + + it "renders a generic icon" do + expect(result).to include("svg#icon-question-mark") + end + end + + context "when the resource is a comment" do + let(:resource) { build :comment } + + it "renders a comment icon" do + expect(result).to include("svg#icon-comment-square") + end + end + + context "when the resource is an initiative" do + let(:resource) { build :initiative } + + it "renders an initiative icon" do + expect(result).to include("svg#icon-initiatives") + end + end + + context "and in other cases" do + let(:resource) { "Something" } + + it "renders a generic icon" do + expect(result).to include("svg#icon-bell") + end + end + end + end + end +end diff --git a/spec/shared/update_initiative_answer_example.rb b/spec/shared/update_initiative_answer_example.rb new file mode 100644 index 0000000000..5eab077786 --- /dev/null +++ b/spec/shared/update_initiative_answer_example.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +shared_examples "update an initiative answer" do + let(:organization) { create(:organization) } + let(:initiative) { create(:initiative, organization: organization, state: state) } + let(:form) do + form_klass.from_params( + form_params + ).with_context( + current_organization: organization, + initiative: initiative + ) + end + let(:signature_end_date) { Date.current + 500.days } + let(:state) { "published" } + let(:form_params) do + { + signature_start_date: Date.current + 10.days, + signature_end_date: signature_end_date, + answer: { en: "Measured answer" }, + answer_url: "http://decidim.org" + } + end + let(:administrator) { create(:user, :admin, organization: organization) } + let(:current_user) { administrator } + let(:command) { described_class.new(initiative, form, current_user) } + + describe "call" do + describe "when the form is not valid" do + before do + allow(form).to receive(:invalid?).and_return(true) + end + + it "broadcasts invalid" do + expect { command.call }.to broadcast(:invalid) + end + + it "doesn't updates the initiative" do + command.call + + form_params.each do |key, value| + expect(initiative[key]).not_to eq(value) + end + end + end + + describe "when the form is valid" do + it "broadcasts ok" do + expect { command.call }.to broadcast(:ok) + end + + it "updates the initiative" do + command.call + initiative.reload + + expect(initiative.answer["en"]).to eq(form_params[:answer][:en]) + expect(initiative.answer_url).to eq(form_params[:answer_url]) + end + + context "when initiative is not published" do + let(:state) { "validating" } + + it "voting interval remains unchanged" do + command.call + initiative.reload + + [:signature_start_date, :signature_end_date].each do |key| + expect(initiative[key]).not_to eq(form_params[key]) + end + end + end + + context "when initiative is published" do + it "voting interval is updated" do + command.call + initiative.reload + + [:signature_start_date, :signature_end_date].each do |key| + expect(initiative[key]).to eq(form_params[key]) + end + end + end + end + end +end From 3357f534a7e7797a33cef8bba09627d2b7a1aecf Mon Sep 17 00:00:00 2001 From: Manuel Tancoigne Date: Fri, 8 Dec 2023 16:35:14 +0100 Subject: [PATCH 08/13] fix: Improve coverage (#455) * fix: Fix deprecated usage of "create_after_upload!" Use "create_and_upload!" instead, wich is an alias. * fix: Write tests for "Decidim::BackupHelper" File: app/helpers/decidim/backup_helper.rb, from 75 to 100%. * fix: Write tests for "Decidim::TranslatorConfigurationHelper" File: lib/decidim/translator_configuration_helper.rb, from 77 to 100%. * fix: Write tests for "Decidim::ActionLogService" File: app/services/decidim/action_log_service.rb, from 77 to 100%. * fix: Write tests for task "decidim_app:k8s:install" * fix: Write tests for "Decidim::DatabaseService" * fix: Write tests for "Decidim::RSpecRunner" * feat(tests): Add helper to skip test when class is missing * fix: Write tests for "DecidimApp::DecidimInitiatives" * fix: Write tests for "SentrySetup" * fix: Write tests for "decidim:db:*" tasks * fix: Rename some tasks test files Moved the test files in sub-directories to keep the same structure as the task name * fix(tasks): Add description for "decidim:repair:url_in_content" * fix(tasks): Correct typo in variable name * fix(tasks): Add space in logged ID separator * fix: Complete tests for tasks "decidim:repair:*" * fix(tasks): Extract classes from "decidim:db:migrate" task file * fix(style): Make Rubocop happy This is a separate commit to keep "rails_migration.rb" as is after its extraction from the Rake task. * fix(migrations_fixer): Raise exceptions instead of exiting * fix: Write tests for "MigrationsFixer" * fix: Write tests for "RailsMigrations" * fix(jobs): Remove "MachineTranslationResourceJob" It is a duplicate from `decidim-core` * fix: Remove "ActiveStorage::Downloadable" class The way it was included in the app (`include` instead of `prepend`) was not overriding the original method. As it does not work with the override (test suite fails), it is removed. * feat(documentation): Add notes on code contribution tools * fix: Write tests for "Decidim::S3RetentionService" * fix: Write tests for "Decidim::S3SyncService" * fix: Ignore "import.rake" in coverage * fix(migrations_fixer): Fix typo --- .../machine_translation_resource_job.rb | 120 ------------- config/application.rb | 5 - docs/CONTRIBUTING.md | 2 + docs/CONTRIBUTING_TO_CODE.md | 40 +++++ lib/active_storage/downloadable.rb | 9 - lib/decidim_app/sentry_setup.rb | 24 +-- lib/migrations_fixer.rb | 87 +++++++++ lib/rails_migrations.rb | 83 +++++++++ lib/tasks/migrate.rake | 166 +----------------- lib/tasks/repair_data.rake | 11 +- spec/helpers/decidim/backup_helper_spec.rb | 55 ++++++ spec/lib/decidim/content_fixer_spec.rb | 2 +- .../translator_configuration_helper_spec.rb | 86 +++++++++ .../decidim_app/decidim_initiatives_spec.rb | 70 ++++++++ spec/lib/decidim_app/sentry_setup_spec.rb | 17 ++ spec/lib/migrations_fixer_spec.rb | 72 ++++++++ spec/lib/rails_migrations_spec.rb | 93 ++++++++++ spec/lib/rspec_runner_spec.rb | 32 ++++ .../tasks/decidim/db/admin_log/clear_spec.rb | 19 ++ .../decidim/db/admin_log/orphans_spec.rb | 19 ++ .../decidim/db/notification/clear_spec.rb | 19 ++ .../decidim/db/notification/orphans_spec.rb | 19 ++ .../tasks/decidim/db/surveys/clear_spec.rb | 19 ++ .../tasks/decidim/db/surveys/orphans_spec.rb | 19 ++ .../lib/tasks/decidim/repair/comments_spec.rb | 45 +++++ .../lib/tasks/decidim/repair/nickname_spec.rb | 45 +++++ .../repair/translations_spec.rb} | 32 ++++ .../repair/url_in_content_spec.rb} | 0 .../decidim_app/k8s/install_task_spec.rb | 15 ++ .../decidim/action_log_service_spec.rb | 12 ++ .../services/decidim/database_service_spec.rb | 27 +++ .../decidim/s3_retention_service_spec.rb | 80 +++++++++ spec/services/decidim/s3_sync_service_spec.rb | 166 ++++++++++++++++++ spec/spec_helper.rb | 1 + spec/support/skip_if_undefined_helper.rb | 8 + 35 files changed, 1204 insertions(+), 315 deletions(-) delete mode 100644 app/jobs/decidim/machine_translation_resource_job.rb create mode 100644 docs/CONTRIBUTING_TO_CODE.md delete mode 100644 lib/active_storage/downloadable.rb create mode 100644 lib/migrations_fixer.rb create mode 100644 lib/rails_migrations.rb create mode 100644 spec/helpers/decidim/backup_helper_spec.rb create mode 100644 spec/lib/decidim/translator_configuration_helper_spec.rb create mode 100644 spec/lib/migrations_fixer_spec.rb create mode 100644 spec/lib/rails_migrations_spec.rb create mode 100644 spec/lib/tasks/decidim/db/admin_log/clear_spec.rb create mode 100644 spec/lib/tasks/decidim/db/admin_log/orphans_spec.rb create mode 100644 spec/lib/tasks/decidim/db/notification/clear_spec.rb create mode 100644 spec/lib/tasks/decidim/db/notification/orphans_spec.rb create mode 100644 spec/lib/tasks/decidim/db/surveys/clear_spec.rb create mode 100644 spec/lib/tasks/decidim/db/surveys/orphans_spec.rb create mode 100644 spec/lib/tasks/decidim/repair/comments_spec.rb create mode 100644 spec/lib/tasks/decidim/repair/nickname_spec.rb rename spec/lib/tasks/{repair_data_translations_spec.rb => decidim/repair/translations_spec.rb} (55%) rename spec/lib/tasks/{repair_data_url_in_content_spec.rb => decidim/repair/url_in_content_spec.rb} (100%) create mode 100644 spec/lib/tasks/decidim_app/k8s/install_task_spec.rb create mode 100644 spec/services/decidim/s3_retention_service_spec.rb create mode 100644 spec/services/decidim/s3_sync_service_spec.rb create mode 100644 spec/support/skip_if_undefined_helper.rb diff --git a/app/jobs/decidim/machine_translation_resource_job.rb b/app/jobs/decidim/machine_translation_resource_job.rb deleted file mode 100644 index b1ba73484b..0000000000 --- a/app/jobs/decidim/machine_translation_resource_job.rb +++ /dev/null @@ -1,120 +0,0 @@ -# frozen_string_literal: true - -module Decidim - # This job is part of the machine translation flow. This one is fired every - # time a `Decidim::TranslatableResource` is created or updated. If any of the - # attributes defines as translatable is modified, then for each of those - # attributes this job will schedule a `Decidim::MachineTranslationFieldsJob`. - class MachineTranslationResourceJob < ApplicationJob - queue_as :translations - - # rubocop: disable Metrics/CyclomaticComplexity - - # Performs the job. - # - # resource - Any kind of `Decidim::TranslatableResource` model instance - # previous_changes - A Hash with the set fo changes. This is intended to be - # taken from `resource.previous_changes`, but we need to manually pass - # them to the job because the value gets lost when serializing the - # resource. - # source_locale - A Symbol representing the source locale for the translation - def perform(resource, previous_changes, source_locale) - return unless Decidim.machine_translation_service_klass - - @resource = resource - @locales_to_be_translated = [] - translatable_fields = @resource.class.translatable_fields_list.map(&:to_s) - translatable_fields.each do |field| - next unless @resource[field].is_a?(Hash) && previous_changes.keys.include?(field) - - translated_locales = translated_locales_list(field) - remove_duplicate_translations(field, translated_locales) if @resource[field]["machine_translations"].present? - - next unless default_locale_changed_or_translation_removed(previous_changes, field) - - @locales_to_be_translated += pending_locales(translated_locales) if @locales_to_be_translated.blank? - - @locales_to_be_translated.each do |target_locale| - Decidim::MachineTranslationFieldsJob.perform_later( - @resource, - field, - resource_field_value( - previous_changes, - field, - source_locale - ), - target_locale, - source_locale - ) - end - end - end - # rubocop: enable Metrics/CyclomaticComplexity - - def default_locale_changed_or_translation_removed(previous_changes, field) - default_locale = default_locale(@resource) - values = previous_changes[field] - old_value = values.first - new_value = values.last - return true unless old_value.is_a?(Hash) - - return true if old_value[default_locale] != new_value[default_locale] - - # In a case where the default locale is not changed - # but a translation of a different locale is deleted - # We trigger a job to translate only for that locale - if old_value[default_locale] == new_value[default_locale] - locales_present = old_value.keys - locales_present.each do |locale| - @locales_to_be_translated << locale if old_value[locale] != new_value[locale] && new_value[locale] == "" - end - end - - @locales_to_be_translated.present? - end - - def resource_field_value(previous_changes, field, source_locale) - values = previous_changes[field] - new_value = values.last - if new_value.is_a?(Hash) - locale = source_locale || default_locale(@resource) - return new_value[locale] - end - - new_value - end - - def default_locale(resource) - if resource.respond_to? :organization - resource.organization.default_locale.to_s - else - Decidim.available_locales.first.to_s - end - end - - def translated_locales_list(field) - return nil unless @resource[field].is_a? Hash - - translated_locales = [] - existing_locales = @resource[field].keys - ["machine_translations"] - existing_locales.each do |locale| - translated_locales << locale if @resource[field][locale].present? - end - - translated_locales - end - - def remove_duplicate_translations(field, translated_locales) - machine_translated_locale = @resource[field]["machine_translations"].keys - unless (translated_locales & machine_translated_locale).nil? - (translated_locales & machine_translated_locale).each { |key| @resource[field]["machine_translations"].delete key } - end - end - - def pending_locales(translated_locales) - available_locales = @resource.organization.available_locales.map(&:to_s) if @resource.respond_to? :organization - available_locales ||= Decidim.available_locales.map(&:to_s) - available_locales - translated_locales - end - end -end diff --git a/config/application.rb b/config/application.rb index ac2b3c6f06..c08cae0cf7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -7,7 +7,6 @@ require "action_cable/engine" # require "action_mailbox/engine" # require "action_text/engine" -require_relative "../lib/active_storage/downloadable" require "wicked_pdf" @@ -39,10 +38,6 @@ class Application < Rails::Application # -- all .rb files in that directory are automatically loaded after loading # the framework and any gems in your application. - config.to_prepare do - ActiveStorage::Blob.include ActiveStorage::Downloadable - end - config.after_initialize do require "extends/controllers/decidim/devise/sessions_controller_extends" require "extends/controllers/decidim/editor_images_controller_extends" diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 92e5d2cb23..b4933ae135 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -19,6 +19,8 @@ If you haven't already, come find the Decidim community in [Matrix](https://app. * Once all checks are green (GG!), please mark your PR as "Ready for review". * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. +The [contributing to code](./CONTRIBUTING_TO_CODE.md) documentation contains notes and examples on the tools we use. + ## Do you intend to add a new feature or change an existing one? * 🇫🇷 Please create a new feature proposal on our french open roadmap [here](https://club.decidim.opensourcepolitics.eu/assemblies/feuille-de-route/f/232/) * 🌎 Create a proposal on the [Meta.decidim platform](https://meta.decidim.org/processes/roadmap/f/122/), to see if it can be accepted in the core of the software diff --git a/docs/CONTRIBUTING_TO_CODE.md b/docs/CONTRIBUTING_TO_CODE.md new file mode 100644 index 0000000000..15957d919b --- /dev/null +++ b/docs/CONTRIBUTING_TO_CODE.md @@ -0,0 +1,40 @@ +# Contributing to code + +This document is a work in progress + +## Unit tests + +We use RSpec to run all our tests: + +```sh +bundle exec rake test:run +# or +bundle exec rspec +``` + +System tests are run on a Chrome/Chromium browser. The chromedriver corresponding to your version is required and should be available in the `$PATH`. + +To run tests without system tests: + +```sh +bundle exec rails assets:precompile + +# Then: +bundle exec rake "test:run[exclude, spec/system/**/*_spec.rb]" +# or +bundle exec rspec --tag ~type:system +``` + +To replay failed tests, use the `--next-failure` flag. + +### Code coverage + +To generate code coverage, use the `SIMPLECOV=1` environment variable when starting tests. + +## Linters + +We use Rubocop to lint Ruby files: + +```sh +bundle exec rubocop +``` diff --git a/lib/active_storage/downloadable.rb b/lib/active_storage/downloadable.rb deleted file mode 100644 index 8969779359..0000000000 --- a/lib/active_storage/downloadable.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module ActiveStorage - module Downloadable - def open(tempdir: nil, &block) - ActiveStorage::Downloader.new(self, tempdir: tempdir).download_blob_to_tempfile(&block) - end - end -end diff --git a/lib/decidim_app/sentry_setup.rb b/lib/decidim_app/sentry_setup.rb index 5f614a1620..1b4da5fb2e 100644 --- a/lib/decidim_app/sentry_setup.rb +++ b/lib/decidim_app/sentry_setup.rb @@ -15,17 +15,7 @@ def init config.traces_sample_rate = sample_rate.to_f - config.traces_sampler = lambda do |sampling_context| - transaction_context = sampling_context[:transaction_context] - op = transaction_context[:op] - transaction_name = transaction_context[:name] - - if op =~ /http/ && transaction_name == "/health_check" - 0.0 - else - sample_rate.to_f - end - end + config.traces_sampler = ->(sampling_context) { sample_trace(sampling_context) } end Sentry.set_tags("server.hostname": hostname) if hostname.present? @@ -34,6 +24,18 @@ def init private + def sample_trace(sampling_context) + transaction_context = sampling_context[:transaction_context] + op = transaction_context[:op] + transaction_name = transaction_context[:name] + + if op =~ /http/ && transaction_name == "/health_check" + 0.0 + else + sample_rate.to_f + end + end + def server_metadata JSON.parse(`scw-metadata-json`) rescue Errno::ENOENT, TypeError diff --git a/lib/migrations_fixer.rb b/lib/migrations_fixer.rb new file mode 100644 index 0000000000..9978421b8f --- /dev/null +++ b/lib/migrations_fixer.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# MigrationsFixer allows to ensure rake task has needed information to success. +class MigrationsFixer + attr_accessor :migrations_path, :logger + + def initialize(logger) + @logger = logger + @migrations_path = Rails.root.join(migrations_folder) + validate! + @osp_app_path = osp_app_path + end + + # Validate configuration before executing task + def validate! + raise "Undefined logger" if @logger.blank? + + validate_migration_path + validate_env_vars + validate_osp_app_path + end + + # Build osp-app path and returns osp-app path ending with '/*' + def osp_app_path + osp_app_path ||= File.expand_path(ENV.fetch("MIGRATIONS_PATH", nil)) + if osp_app_path.end_with?("/") + osp_app_path + else + "#{osp_app_path}/" + end + end + + private + + # Ensure MIGRATIONS_PATH is correctly set + def validate_env_vars + if ENV["MIGRATIONS_PATH"].blank? + @logger.error("You must specify ENV var 'MIGRATIONS_PATH'") + + @logger.fatal(helper) + validation_failed + end + end + + # Ensure osp_app path exists + def validate_osp_app_path + unless File.directory?(osp_app_path) + @logger.fatal("Directory '#{osp_app_path}' not found, aborting task...") + validation_failed + end + end + + # Ensure migrations path exists + def validate_migration_path + unless File.directory? @migrations_path + @logger.error("Directory '#{@migrations_path}' not found, aborting task...") + @logger.error("Please see absolute path '#{File.expand_path(@migrations_path)}'") + + @logger.fatal("Please ensure the migration path is correctly defined.") + validation_failed + end + end + + # Returns path to DB migrations (default: "db/migrate") + def migrations_folder + ActiveRecord::Base.connection.migration_context.migrations_paths.first + end + + # Display helper + def helper + "Manual : decidim:db:migrate +Fix migrations issue when switching from osp-app to decidim-app. Rake task will automatically save already passed migrations from current project that are marked as 'down'. +Then it will try to migrate each 'down' version, if it fails, it automatically note as 'up' + +Parameters: +* MIGRATIONS_PATH - String [Relative or absolute path] : Pass to previous decidim project + +Example: bundle exec rake decidim:db:migrate MIGRATIONS_PATH='../osp-app/db/migrate' +or +bundle exec rake decidim:db:migrate MIGRATIONS_PATH='/Users/toto/osp-app/db/migrate' +" + end + + def validation_failed + raise "Invalid configuration, aborting" + end +end diff --git a/lib/rails_migrations.rb b/lib/rails_migrations.rb new file mode 100644 index 0000000000..d4f4fe5faf --- /dev/null +++ b/lib/rails_migrations.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +# RailsMigrations deals with migrations of the project +class RailsMigrations + attr_accessor :fetch_all + + def initialize(migration_fixer) + @fetch_all = migration_status + @migration_fixer = migration_fixer + end + + # Reload down migrations according to the new migration status + def reload_down! + @down = nil + reload_migrations! + down + end + + # Return all migrations marked as 'down' + def down + @down ||= @fetch_all&.map do |migration_ary| + migration_ary if migration_ary&.first == "down" + end&.compact + end + + # Refresh all migrations according to DB + def reload_migrations! + @fetch_all = migration_status + end + + # Print migrations status + def display_status! + @fetch_all&.each do |status, version, name| + @migration_fixer.logger.info("#{status.center(8)} #{version.ljust(14)} #{name}") + end + end + + # Returns all migration present in DB but with no migration files defined + def not_found + @not_found ||= @fetch_all&.map { |_, version, name| version if name.include?("NO FILE") }&.compact + end + + # returns all versions marked as 'down' but already passed in past + # This methods is based on migration filenames from osp-app folder, then compare with current migration folder and retrieve duplicated migration with another version number + # Returns array of 'down' versions + def versions_down_but_already_passed + needed_migrations = already_accepted_migrations&.map do |migration| + Dir.glob("#{@migration_fixer.migrations_path}/*#{migration_name_for(migration)}") + end&.flatten! + + needed_migrations&.map { |filename| migration_version_for(filename) } + end + + private + + # returns the migration name based on migration version + # Example for migration : 11111_add_item_in_class + # @return : add_item_in_class + def migration_name_for(migration) + migration.split("/")[-1].split("_")[1..-1].join("_") + end + + # Returns the migration version based on migration filename + # Example for migration : 11111_add_item_in_class + # @return : 11111 + def migration_version_for(migration) + migration.split("/")[-1].split("_")[0] + end + + # returns migrations filename from old osp-app folder, based on versions present in database with no file related + def already_accepted_migrations + @already_accepted_migrations ||= not_found&.map do |migration| + osp_app = Dir.glob("#{@migration_fixer.osp_app_path}*")&.select { |path| path if path.include?(migration) } + + osp_app.first if osp_app.present? + end&.compact + end + + # Fetch all migrations statuses + def migration_status + ActiveRecord::Base.connection.migration_context.migrations_status + end +end diff --git a/lib/tasks/migrate.rake b/lib/tasks/migrate.rake index b54955a3ee..c37eea39d7 100644 --- a/lib/tasks/migrate.rake +++ b/lib/tasks/migrate.rake @@ -1,5 +1,6 @@ # frozen_string_literal: true +# :nocov: namespace :decidim do namespace :db do desc "Migrate Database" @@ -68,167 +69,4 @@ namespace :decidim do end end end - -# RailsMigrations deals with migrations of the project -class RailsMigrations - attr_accessor :fetch_all - - def initialize(migration_fixer) - @fetch_all = migration_status - @migration_fixer = migration_fixer - end - - # Reload down migrations according to the new migration status - def reload_down! - @down = nil - reload_migrations! - down - end - - # Return all migrations marked as 'down' - def down - @down ||= @fetch_all&.map do |migration_ary| - migration_ary if migration_ary&.first == "down" - end.compact - end - - # Refresh all migrations according to DB - def reload_migrations! - @fetch_all = migration_status - end - - # Print migrations status - def display_status! - @fetch_all&.each do |status, version, name| - @migration_fixer.logger.info("#{status.center(8)} #{version.ljust(14)} #{name}") - end - end - - # Returns all migration present in DB but with no migration files defined - def not_found - @not_found ||= @fetch_all&.map { |_, version, name| version if name.include?("NO FILE") }.compact - end - - # returns all versions marked as 'down' but already passed in past - # This methods is based on migration filenames from osp-app folder, then compare with current migration folder and retrieve duplicated migration with another version number - # Returns array of 'down' versions - def versions_down_but_already_passed - needed_migrations = already_accepted_migrations&.map do |migration| - Dir.glob("#{@migration_fixer.migrations_path}/*#{migration_name_for(migration)}") - end.flatten! - - needed_migrations&.map { |filename| migration_version_for(filename) } - end - - private - - # returns the migration name based on migration version - # Example for migration : 11111_add_item_in_class - # @return : add_item_in_class - def migration_name_for(migration) - migration.split("/")[-1].split("_")[1..-1].join("_") - end - - # Returns the migration version based on migration filename - # Example for migration : 11111_add_item_in_class - # @return : 11111 - def migration_version_for(migration) - migration.split("/")[-1].split("_")[0] - end - - # returns migrations filename from old osp-app folder, based on versions present in database with no file related - def already_accepted_migrations - @already_accepted_migrations ||= not_found&.map do |migration| - osp_app = Dir.glob("#{@migration_fixer.osp_app_path}*")&.select { |path| path if path.include?(migration) } - - osp_app.first if osp_app.present? - end.compact - end - - # Fetch all migrations statuses - def migration_status - ActiveRecord::Base.connection.migration_context.migrations_status - end -end - -# MigrationsFixer allows to ensure rake task has needed information to success. -class MigrationsFixer - attr_accessor :migrations_path, :logger - - def initialize(logger) - @logger = logger - @migrations_path = Rails.root.join(migrations_folder) - validate! - @osp_app_path = osp_app_path - end - - # Validate configuration before executing task - def validate! - raise "Undefined logger" if @logger.blank? - - validate_migration_path - validate_env_vars - validate_osp_app_path - end - - # Build osp-app path and returns osp-app path ending with '/*' - def osp_app_path - osp_app_path ||= File.expand_path(ENV.fetch("MIGRATIONS_PATH", nil)) - if osp_app_path.end_with?("/") - osp_app_path - else - "#{osp_app_path}/" - end - end - - private - - # Ensure MIGRATIONS_PATH is correctly set - def validate_env_vars - if ENV["MIGRATIONS_PATH"].blank? - @logger.error("You must specify ENV var 'MIGRATIONS_PATH'") - - @logger.fatal(helper) - exit 2 - end - end - - # Ensure osp_app path exists - def validate_osp_app_path - unless File.directory?(osp_app_path) - @logger.fatal("Directory '#{osp_app_path}' not found, aborting task...") - exit 2 - end - end - - # Ensure migrations path exists - def validate_migration_path - unless File.directory? @migrations_path - @logger.error("Directory '#{@migrations_path}' not found, aborting task...") - @logger.error("Please see absolute path '#{File.expand_path(@migrations_path)}'") - - @logger.fatal("Please ensure the migration path is correctly defined.") - exit 2 - end - end - - # Returns path to DB migrations (default: "db/migrate") - def migrations_folder - ActiveRecord::Base.connection.migration_context.migrations_paths.first - end - - # Display helper - def helper - "Manual : decidim:db:migrate -Fix migrations issue when switching from osp-app to decidim-app. Rake task will automatically save already passed migrations from current project that are marked as 'down'. -Then it will try to migrate each 'down' version, if it fails, it automatically note as 'up' - -Parametes: -* MIGRATIONS_PATH - String [Relative or absolute path] : Pass to previous decidim project - -Example: bundle exec rake decidim:db:migrate MIGRATIONS_PATH='../osp-app/db/migrate' -or -bundle exec rake decidim:db:migrate MIGRATIONS_PATH='/Users/toto/osp-app/db/migrate' -" - end -end +# :nocov: diff --git a/lib/tasks/repair_data.rake b/lib/tasks/repair_data.rake index 0019292440..faee990540 100644 --- a/lib/tasks/repair_data.rake +++ b/lib/tasks/repair_data.rake @@ -7,13 +7,13 @@ namespace :decidim do logger = Logger.new($stdout) logger.info("Checking all nicknames...") - udpated_user_ids = Decidim::RepairNicknameService.run + updated_user_ids = Decidim::RepairNicknameService.run - if udpated_user_ids.blank? + if updated_user_ids.blank? logger.info("No users updated") else - logger.info("#{udpated_user_ids.count} users updated") - logger.info("Updated users ID : #{udpated_user_ids.join(", ")}") + logger.info("#{updated_user_ids.count} users updated") + logger.info("Updated users ID : #{updated_user_ids.join(", ")}") end logger.info("Operation terminated") @@ -30,7 +30,7 @@ namespace :decidim do logger.info("No comments updated") else logger.info("#{updated_comments_ids} comments updated") - logger.info("Updated comments ID : #{updated_comments_ids.join(",")}") + logger.info("Updated comments ID : #{updated_comments_ids.join(", ")}") end logger.info("Operation terminated") @@ -57,6 +57,7 @@ namespace :decidim do end end + desc 'Replaces "@deprecated_endpoint" in every database columns with the right blob URL' task url_in_content: :environment do logger = Logger.new($stdout) deprecated_hosts = ENV["DEPRECATED_OBJECTSTORE_S3_HOSTS"].to_s.split(",").map(&:strip) diff --git a/spec/helpers/decidim/backup_helper_spec.rb b/spec/helpers/decidim/backup_helper_spec.rb new file mode 100644 index 0000000000..6e18739662 --- /dev/null +++ b/spec/helpers/decidim/backup_helper_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Decidim::BackupHelper do + include Decidim::BackupHelper + + let(:hostname) { Socket.gethostname.encode("utf-8") } + let(:dir_name) { "test project" } + let(:branch) { "some_branch" } + + let!(:temp_dir) do + dir = Dir.mktmpdir("decidim-tests-") + FileUtils.cd dir do + # Use another, known directory + FileUtils.mkdir_p dir_name + `cd "#{dir_name}" && git init && git checkout -b #{branch}}` + end + File.join(dir, dir_name) + end + + after do + FileUtils.rm_rf File.dirname(temp_dir) + end + + describe "#generate_subfolder_name" do + context "with an existing Git repository" do + it "returns the right string" do + expected = "#{hostname.parameterize}--#{dir_name.parameterize}--#{branch.parameterize}" + + FileUtils.cd temp_dir do + expect(generate_subfolder_name).to eq expected + end + end + end + + context "without a Git repository" do + # it "raises an exception" do + # FileUtils.cd File.dirname(temp_dir) do + # expect do + # generate_subfolder_name + # end.to raise_error + # end + # end + + it "returns an incomplete string" do + expected = "#{hostname.parameterize}----" + + FileUtils.cd File.dirname(temp_dir) do + expect(generate_subfolder_name).to eq expected + end + end + end + end +end diff --git a/spec/lib/decidim/content_fixer_spec.rb b/spec/lib/decidim/content_fixer_spec.rb index 323a3e589a..31829d88e5 100644 --- a/spec/lib/decidim/content_fixer_spec.rb +++ b/spec/lib/decidim/content_fixer_spec.rb @@ -13,7 +13,7 @@ let(:invalid_body_comment) { { en: "

Here is a not valid comment with Link text

" } } let(:content) { "

Here is a not valid comment with Link text

" } let(:deprecated_url) { "https://#{deprecated_endpoint}/xxxx?response-content-disposition=inline%3Bfilename%3D\"BuPa23_reglement-interieur.pdf\"%3Bfilename*%3DUTF-8''BuPa23_r%25C3%25A8glement-int%25C3%25A9rieur.pdf&response-content-type=application%2Fpdf" } - let!(:blob) { ActiveStorage::Blob.create_after_upload!(filename: "BuPa23_reglement-interieur.pdf", io: File.open("spec/fixtures/BuPa23_reglement-interieur.pdf"), content_type: "application/pdf") } + let!(:blob) { ActiveStorage::Blob.create_and_upload!(filename: "BuPa23_reglement-interieur.pdf", io: File.open("spec/fixtures/BuPa23_reglement-interieur.pdf"), content_type: "application/pdf") } let(:blob_path) { Rails.application.routes.url_helpers.rails_blob_path(ActiveStorage::Blob.find(blob.id), only_path: true) } describe "#repair" do diff --git a/spec/lib/decidim/translator_configuration_helper_spec.rb b/spec/lib/decidim/translator_configuration_helper_spec.rb new file mode 100644 index 0000000000..7d9e90a1a2 --- /dev/null +++ b/spec/lib/decidim/translator_configuration_helper_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "spec_helper" +require "decidim/translator_configuration_helper" + +RSpec.describe Decidim::TranslatorConfigurationHelper do + let!(:original_queue_adapter) { Rails.configuration.active_job.queue_adapter } + let!(:original_enable_machine_translations) { Decidim.enable_machine_translations } + let(:with_incompatible_backend) { Rails.configuration.active_job.queue_adapter = :async } + let(:with_compatible_backend) { Rails.configuration.active_job.queue_adapter = :something } + let(:with_translations_enabled) { Decidim.enable_machine_translations = true } + let(:with_translations_disabled) { Decidim.enable_machine_translations = false } + + after do + Rails.configuration.active_job.queue_adapter = original_queue_adapter + Decidim.enable_machine_translations = original_enable_machine_translations + end + + describe ".able_to_seed?" do + context "when Decidim translations are enabled" do + before { with_translations_enabled } + + context "when the backend is 'async'" do + before { with_incompatible_backend } + + it "raises an error" do + expect do + Decidim::TranslatorConfigurationHelper.able_to_seed? + end.to raise_error RuntimeError, /^You can't seed the database/ + end + end + + context "when the backend is not 'async'" do + before { with_compatible_backend } + + it "returns nil" do + expect(Decidim::TranslatorConfigurationHelper.able_to_seed?).to be_nil + end + end + end + + context "when Decidim translations are disabled" do + before { with_translations_disabled } + + it "returns true" do + expect(Decidim::TranslatorConfigurationHelper.able_to_seed?).to be true + end + end + end + + describe ".compatible_backend" do + context "with an 'async' backend" do + before { with_incompatible_backend } + + it "returns false" do + expect(Decidim::TranslatorConfigurationHelper.compatible_backend?).to be false + end + end + + context "with another backend" do + before { with_compatible_backend } + + it "returns true" do + expect(Decidim::TranslatorConfigurationHelper.compatible_backend?).to be true + end + end + end + + describe ".translator_activated?" do + context "when translations are active" do + before { with_translations_enabled } + + it "returns true" do + expect(Decidim::TranslatorConfigurationHelper.translator_activated?).to be true + end + end + + context "when translations are inactive" do + before { with_translations_disabled } + + it "returns true" do + expect(Decidim::TranslatorConfigurationHelper.translator_activated?).to be false + end + end + end +end diff --git a/spec/lib/decidim_app/decidim_initiatives_spec.rb b/spec/lib/decidim_app/decidim_initiatives_spec.rb index 810e44bbbd..4aa3499a90 100644 --- a/spec/lib/decidim_app/decidim_initiatives_spec.rb +++ b/spec/lib/decidim_app/decidim_initiatives_spec.rb @@ -5,6 +5,17 @@ describe DecidimApp::DecidimInitiatives do subject { described_class } + describe ".apply_configuration" do + it "sets the configuration values" do + skip_if_undefined "Decidim::Initiatives", "decidim-initiatives" + + allow(Decidim::Initiatives).to receive(:configure) + subject.apply_configuration + + expect(Decidim::Initiatives).to have_received(:configure) + end + end + describe "#creation_enabled?" do it "returns true" do expect(subject).to be_creation_enabled @@ -109,6 +120,21 @@ end end + describe ".default_components" do + it "handles empty array string" do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :initiatives, :default_components).and_return(["[]"]) + + expect(subject.default_components).to eq [] + end + + it "returns the configured value" do + expected = ["a", 1, true] + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :initiatives, :default_components).and_return(expected) + + expect(subject.default_components).to eq expected + end + end + describe "#first_notification_percentage" do context "when rails secret '25'" do before do @@ -196,4 +222,48 @@ end end end + + describe ".print_enabled?" do + context "when rails secret has a value" do + [10, true, "hello"].each do |value| + it "returns false for '#{value}'" do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :initiatives, :print_enabled).and_return(value) + + expect(subject.print_enabled?).to be true + end + end + end + + context "when rails secret has no value" do + [false, nil, ""].each do |value| + it "returns false for '#{value}'" do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :initiatives, :print_enabled).and_return(value) + + expect(subject.print_enabled?).to be false + end + end + end + end + + describe ".do_not_require_authorization?" do + context "when rails secret has a value" do + [10, true, "hello"].each do |value| + it "returns false for '#{value}'" do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :initiatives, :do_not_require_authorization).and_return(value) + + expect(subject.do_not_require_authorization?).to be true + end + end + end + + context "when rails secret has no value" do + [false, nil, ""].each do |value| + it "returns false for '#{value}'" do + allow(Rails.application.secrets).to receive(:dig).with(:decidim, :initiatives, :do_not_require_authorization).and_return(value) + + expect(subject.do_not_require_authorization?).to be false + end + end + end + end end diff --git a/spec/lib/decidim_app/sentry_setup_spec.rb b/spec/lib/decidim_app/sentry_setup_spec.rb index 93a23c04d7..ef1a209afd 100644 --- a/spec/lib/decidim_app/sentry_setup_spec.rb +++ b/spec/lib/decidim_app/sentry_setup_spec.rb @@ -52,6 +52,23 @@ end end + describe "#sample_trace" do + let(:transaction_name) { "/some_page" } + let(:context) { { transaction_context: { op: "http", name: transaction_name } } } + + context "when transaction is about the health check" do + let(:transaction_name) { "/health_check" } + + it "returns 0" do + expect(subject.send(:sample_trace, context)).to eq 0.0 + end + end + + it "returns a Float" do + expect(subject.send(:sample_trace, context)).to be_a Float + end + end + describe ".ip" do it "returns the ip" do expect(subject.send(:ip)).to eq("123.123.123.123") diff --git a/spec/lib/migrations_fixer_spec.rb b/spec/lib/migrations_fixer_spec.rb new file mode 100644 index 0000000000..f926860d13 --- /dev/null +++ b/spec/lib/migrations_fixer_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require "spec_helper" + +require "migrations_fixer" + +RSpec.describe MigrationsFixer do + let(:logger) { Logger.new nil } + let(:migration_path_env) { Rails.root.to_s } + let(:instance) { described_class.new logger } + + before do + @old_migration_path_env = ENV.fetch("MIGRATIONS_PATH", nil) + ENV["MIGRATIONS_PATH"] = migration_path_env + end + + after do + ENV["MIGRATIONS_PATH"] = @old_migration_path_env # rubocop:disable RSpec/InstanceVariable + end + + describe ".new" do + context "with valid parameters" do + it "sets the logger" do + expect(instance.logger).to eq logger + end + + it "sets the migrations path" do + expect(instance.migrations_path).not_to be_blank + end + end + + context "with missing logger" do + let(:logger) { nil } + + it "raises an exception" do + expect do + described_class.new logger + end.to raise_error "Undefined logger" + end + end + + context "with missing environment" do + let(:migration_path_env) { nil } + + it "raises an exception" do + expect do + described_class.new logger + end.to raise_error "Invalid configuration, aborting" + end + end + + context "with non-existing MIGRATIONS_PATH variable" do + let(:migration_path_env) { "/some/inexistant/dir" } + + it "raises an exception" do + expect do + described_class.new logger + end.to raise_error "Invalid configuration, aborting" + end + end + + context "with missing project migrations" do + it "raises an exception" do + allow(ActiveRecord::Base.connection.migration_context.migrations_paths).to receive(:first).and_return "/some/invalid/directory" + + expect do + described_class.new logger + end.to raise_error "Invalid configuration, aborting" + end + end + end +end diff --git a/spec/lib/rails_migrations_spec.rb b/spec/lib/rails_migrations_spec.rb new file mode 100644 index 0000000000..f47d15c488 --- /dev/null +++ b/spec/lib/rails_migrations_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require "spec_helper" + +require "rails_migrations" +require "migrations_fixer" + +class FakeMigrationsFixer + attr_reader :logger + + def initialize(logger) + @logger = logger + end + + def osp_app_path + "/some/dir" + end + + def migrations_path + "/something_else" + end +end + +RSpec.describe RailsMigrations do + let(:logger) { Logger.new nil } + let(:migration_fixer) { FakeMigrationsFixer.new logger } + let(:instance) { described_class.new migration_fixer } + let(:migrations_status) do + [ + ["up", "20230824135802", "Change something in db structure"], + ["down", "20230824135803", "Change something else"], + ["down", "20230824135804", "********** NO FILE **********"] + ] + end + + before do + allow(instance).to receive(:migration_status).and_return migrations_status + instance.reload_migrations! + end + + describe "#reload_down!" do + it "reloads and find down migrations" do + allow(instance).to receive(:reload_migrations!) + allow(instance).to receive(:down) + + instance.reload_down! + + aggregate_failures do + expect(instance).to have_received(:reload_migrations!) + expect(instance).to have_received(:down) + end + end + end + + describe "#down" do + it "returns all migrations marked 'down'" do + expect(instance.down.size).to eq 2 + end + end + + describe "#reload_migrations!" do + it "resets @fetch_all" do + new_list = [1, 2, 3] + allow(instance).to receive(:migration_status).and_return new_list + + instance.reload_migrations! + expect(instance.fetch_all).to eq new_list + end + end + + describe "#display_status!" do + it "logs statuses" do + allow(logger).to receive(:info) + + instance.display_status! + + expect(logger).to have_received(:info).exactly(3).times + end + end + + describe "#not_found" do + it "returns the amount of missing migrations files" do + expect(instance.not_found.size).to eq 1 + end + end + + describe "#versions_down_but_already_passed" do + it "returns the list of possible files for missing versions" do + allow(Dir).to receive(:glob).and_return ["20230824135804_change_something_else_again.rb"] + expect(instance.versions_down_but_already_passed).to eq ["20230824135804"] + end + end +end diff --git a/spec/lib/rspec_runner_spec.rb b/spec/lib/rspec_runner_spec.rb index 70db486aa5..5411e53473 100644 --- a/spec/lib/rspec_runner_spec.rb +++ b/spec/lib/rspec_runner_spec.rb @@ -69,6 +69,38 @@ module Decidim end end + describe "#for" do + context "with missing arguments" do + it "fails without pattern" do + expect do + described_class.for nil, mask, slice + end.to raise_error("Missing pattern") + end + + it "fails without mask" do + expect do + described_class.for pattern, nil, slice + end.to raise_error("Missing mask") + end + + it "fails without slice" do + expect do + described_class.for pattern, mask, nil + end.to raise_error("Missing slice") + end + end + + context "with all the arguments" do + # This is tightly coupled with the implementation + it "runs the suite" do + allow(described_class).to receive(:new).and_return subject + allow(subject).to receive(:run).and_return "__success__" + + expect(described_class.for(pattern, mask, slice)).to eq "__success__" + end + end + end + describe "#sliced_files" do before do allow(Dir).to receive(:glob).and_return(files) diff --git a/spec/lib/tasks/decidim/db/admin_log/clear_spec.rb b/spec/lib/tasks/decidim/db/admin_log/clear_spec.rb new file mode 100644 index 0000000000..2f03d7b1a0 --- /dev/null +++ b/spec/lib/tasks/decidim/db/admin_log/clear_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "rake decidim:db:admin_log:clean", type: :task do + it "preloads the Rails environment" do + expect(task.prerequisites).to include "environment" + end + + it "invokes the 'clear' method" do + stub = Decidim::ActionLogService.new + allow(Decidim::ActionLogService).to receive(:new).and_return stub + allow(stub).to receive(:clear).and_return(true) + + task.execute + + expect(stub).to have_received(:clear) + end +end diff --git a/spec/lib/tasks/decidim/db/admin_log/orphans_spec.rb b/spec/lib/tasks/decidim/db/admin_log/orphans_spec.rb new file mode 100644 index 0000000000..1acb661c93 --- /dev/null +++ b/spec/lib/tasks/decidim/db/admin_log/orphans_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "rake decidim:db:admin_log:orphans", type: :task do + it "preloads the Rails environment" do + expect(task.prerequisites).to include "environment" + end + + it "invokes the 'orphans' method" do + stub = Decidim::ActionLogService.new + allow(Decidim::ActionLogService).to receive(:new).and_return stub + allow(stub).to receive(:orphans).and_return(true) + + task.execute + + expect(stub).to have_received(:orphans) + end +end diff --git a/spec/lib/tasks/decidim/db/notification/clear_spec.rb b/spec/lib/tasks/decidim/db/notification/clear_spec.rb new file mode 100644 index 0000000000..9b5b9d29a0 --- /dev/null +++ b/spec/lib/tasks/decidim/db/notification/clear_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "rake decidim:db:notification:clean", type: :task do + it "preloads the Rails environment" do + expect(task.prerequisites).to include "environment" + end + + it "invokes the 'clear' method" do + stub = Decidim::NotificationService.new + allow(Decidim::NotificationService).to receive(:new).and_return stub + allow(stub).to receive(:clear).and_return(true) + + task.execute + + expect(stub).to have_received(:clear) + end +end diff --git a/spec/lib/tasks/decidim/db/notification/orphans_spec.rb b/spec/lib/tasks/decidim/db/notification/orphans_spec.rb new file mode 100644 index 0000000000..621a1ab025 --- /dev/null +++ b/spec/lib/tasks/decidim/db/notification/orphans_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "rake decidim:db:notification:orphans", type: :task do + it "preloads the Rails environment" do + expect(task.prerequisites).to include "environment" + end + + it "invokes the 'orphans' method" do + stub = Decidim::NotificationService.new + allow(Decidim::NotificationService).to receive(:new).and_return stub + allow(stub).to receive(:orphans).and_return(true) + + task.execute + + expect(stub).to have_received(:orphans) + end +end diff --git a/spec/lib/tasks/decidim/db/surveys/clear_spec.rb b/spec/lib/tasks/decidim/db/surveys/clear_spec.rb new file mode 100644 index 0000000000..a61433cbe8 --- /dev/null +++ b/spec/lib/tasks/decidim/db/surveys/clear_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "rake decidim:db:surveys:clean", type: :task do + it "preloads the Rails environment" do + expect(task.prerequisites).to include "environment" + end + + it "invokes the 'clear' method" do + stub = Decidim::SurveysService.new + allow(Decidim::SurveysService).to receive(:new).and_return stub + allow(stub).to receive(:clear).and_return(true) + + task.execute + + expect(stub).to have_received(:clear) + end +end diff --git a/spec/lib/tasks/decidim/db/surveys/orphans_spec.rb b/spec/lib/tasks/decidim/db/surveys/orphans_spec.rb new file mode 100644 index 0000000000..1d9747995a --- /dev/null +++ b/spec/lib/tasks/decidim/db/surveys/orphans_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "rake decidim:db:surveys:orphans", type: :task do + it "preloads the Rails environment" do + expect(task.prerequisites).to include "environment" + end + + it "invokes the 'orphans' method" do + stub = Decidim::SurveysService.new + allow(Decidim::SurveysService).to receive(:new).and_return stub + allow(stub).to receive(:orphans).and_return(true) + + task.execute + + expect(stub).to have_received(:orphans) + end +end diff --git a/spec/lib/tasks/decidim/repair/comments_spec.rb b/spec/lib/tasks/decidim/repair/comments_spec.rb new file mode 100644 index 0000000000..8b1b7ca10b --- /dev/null +++ b/spec/lib/tasks/decidim/repair/comments_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "rake decidim:repair:comments", type: :task do + it "uses the appropriate service" do + allow(Decidim::RepairCommentsService).to receive(:run) + + task.execute + + expect(Decidim::RepairCommentsService).to have_received(:run).once + end + + describe "logging" do + let!(:logger) { Logger.new($stdout) } + + before do + # Stub the logger + allow(logger).to receive(:info) + allow(Logger).to receive(:new).and_return(logger) + + allow(Decidim::RepairCommentsService).to receive(:run).and_return updated_comments_ids + end + + context "when no nickname was repaired" do + let(:updated_comments_ids) { [] } + + it "logs a message" do + task.execute + + expect(logger).to have_received(:info).with("No comments updated") + end + end + + context "when some nicknames were repaired" do + let(:updated_comments_ids) { [1, 2, 3] } + + it "logs a message" do + task.execute + + expect(logger).to have_received(:info).with("Updated comments ID : 1, 2, 3") + end + end + end +end diff --git a/spec/lib/tasks/decidim/repair/nickname_spec.rb b/spec/lib/tasks/decidim/repair/nickname_spec.rb new file mode 100644 index 0000000000..60f157c18d --- /dev/null +++ b/spec/lib/tasks/decidim/repair/nickname_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "rake decidim:repair:nickname", type: :task do + it "uses the appropriate service" do + allow(Decidim::RepairNicknameService).to receive(:run) + + task.execute + + expect(Decidim::RepairNicknameService).to have_received(:run).once + end + + describe "logging" do + let!(:logger) { Logger.new($stdout) } + + before do + # Stub the logger + allow(logger).to receive(:info) + allow(Logger).to receive(:new).and_return(logger) + + allow(Decidim::RepairNicknameService).to receive(:run).and_return updated_user_ids + end + + context "when no nickname was repaired" do + let(:updated_user_ids) { [] } + + it "logs a message" do + task.execute + + expect(logger).to have_received(:info).with("No users updated") + end + end + + context "when some nicknames were repaired" do + let(:updated_user_ids) { [1, 2, 3] } + + it "logs a message" do + task.execute + + expect(logger).to have_received(:info).with("Updated users ID : 1, 2, 3") + end + end + end +end diff --git a/spec/lib/tasks/repair_data_translations_spec.rb b/spec/lib/tasks/decidim/repair/translations_spec.rb similarity index 55% rename from spec/lib/tasks/repair_data_translations_spec.rb rename to spec/lib/tasks/decidim/repair/translations_spec.rb index 328994515b..f419c48485 100644 --- a/spec/lib/tasks/repair_data_translations_spec.rb +++ b/spec/lib/tasks/decidim/repair/translations_spec.rb @@ -36,4 +36,36 @@ task.execute end end + + describe "logging" do + let!(:logger) { Logger.new($stdout) } + + before do + # Stub the logger + allow(logger).to receive(:info) + allow(Logger).to receive(:new).and_return(logger) + + allow(Decidim::RepairTranslationsService).to receive(:run).and_return updated_resources_ids + end + + context "when no nickname was repaired" do + let(:updated_resources_ids) { [] } + + it "logs a message" do + task.execute + + expect(logger).to have_received(:info).with("No resources updated") + end + end + + context "when some nicknames were repaired" do + let(:updated_resources_ids) { [1, 2, 3] } + + it "logs a message" do + task.execute + + expect(logger).to have_received(:info).with("Enqueued resources : 1, 2, 3") + end + end + end end diff --git a/spec/lib/tasks/repair_data_url_in_content_spec.rb b/spec/lib/tasks/decidim/repair/url_in_content_spec.rb similarity index 100% rename from spec/lib/tasks/repair_data_url_in_content_spec.rb rename to spec/lib/tasks/decidim/repair/url_in_content_spec.rb diff --git a/spec/lib/tasks/decidim_app/k8s/install_task_spec.rb b/spec/lib/tasks/decidim_app/k8s/install_task_spec.rb new file mode 100644 index 0000000000..9386dde5a1 --- /dev/null +++ b/spec/lib/tasks/decidim_app/k8s/install_task_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "rake decidim_app:k8s:install", type: :task do + it "preloads the Rails environment" do + expect(task.prerequisites).to include "environment" + end + + it "calls db:migrate" do + expect(Rake::Task["db:migrate"]).to receive(:invoke) + + task.execute + end +end diff --git a/spec/services/decidim/action_log_service_spec.rb b/spec/services/decidim/action_log_service_spec.rb index 1f0a7de1e6..98d6fd600f 100644 --- a/spec/services/decidim/action_log_service_spec.rb +++ b/spec/services/decidim/action_log_service_spec.rb @@ -40,4 +40,16 @@ end.to change(Decidim::ActionLog, :count).from(10).to(0) end end + + describe "#orphans_for" do + context "when the class does not exist" do + it "logs the error" do + logger = Logger.new($stdout) + allow(logger).to receive(:warn) + described_class.new(logger: logger).send(:orphans_for, "NonExistingClass") + + expect(logger).to have_received(:warn).with("Skipping class : NonExistingClass") + end + end + end end diff --git a/spec/services/decidim/database_service_spec.rb b/spec/services/decidim/database_service_spec.rb index 613f43064e..2e2d13d7ac 100644 --- a/spec/services/decidim/database_service_spec.rb +++ b/spec/services/decidim/database_service_spec.rb @@ -2,6 +2,18 @@ require "spec_helper" +class FakeDatabaseService < Decidim::DatabaseService + def initialize(resource_types: nil, **args) + @resource_types = resource_types + + super(**args) + end + + def resource_types # rubocop:disable Style/TrivialAccessors + @resource_types + end +end + describe Decidim::DatabaseService do subject { described_class.new } @@ -28,4 +40,19 @@ end.to raise_error RuntimeError, "Method clear_data_for isn't defined for Decidim::DatabaseService" end end + + describe "when used as class parent" do + let(:fake_instance) { FakeDatabaseService.new(**instance_args) } + let(:instance_args) { {} } + + describe "#orphans" do + context "with no resource type" do + let(:instance_args) { { resource_types: nil } } + + it "returns nil" do + expect(fake_instance.orphans).to be_nil + end + end + end + end end diff --git a/spec/services/decidim/s3_retention_service_spec.rb b/spec/services/decidim/s3_retention_service_spec.rb new file mode 100644 index 0000000000..c6f46a9c3f --- /dev/null +++ b/spec/services/decidim/s3_retention_service_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Decidim::S3RetentionService do + let(:options) { {} } + let(:instance) { described_class.new(options) } + + describe ".run" do + it "executes the service" do + allow(described_class).to receive(:new).with(options).and_return instance + allow(instance).to receive(:execute).and_return "__success__" + + expect(described_class.run({})).to eq "__success__" + end + end + + describe "#default_options" do + let(:option_keys) { instance.default_options.keys } + + it "returns a hash" do + expect(instance.default_options).to be_a Hash + end + + it "has keys existing in backup configuration" do + config_keys = Rails.application.config.backup[:s3sync].keys + .map { |k| "s3_#{k}".to_sym } + + aggregate_failures do + option_keys.each do |key| + expect(config_keys).to include key + end + end + end + end + + describe "#subfolder" do + it "returns a memoized string" do + subfolder = instance.subfolder + + expect(subfolder).to be_a String + expect(instance.instance_variable_get(:@subfolder)).to eq subfolder + end + + context "with an option given" do + let(:options) { { subfolder: "something" } } + + it "uses the given path" do + expect(instance.subfolder).to eq "something" + end + end + + context "without an option given" do + it "generates a path" do + expect(instance.subfolder).not_to be_blank + end + end + end + + describe "#retention_dates" do + let(:retention_dates) { instance.retention_dates } + + it "returns an array" do + expect(instance.retention_dates).to be_a Array + end + + it "contains no duplicates" do + expect(retention_dates.size).to eq retention_dates.uniq.size + end + end + + describe "#service" do + it "memoizes the storage service" do + allow(Fog::Storage).to receive(:new).and_return("__success__") + + 2.times { instance.send(:service) } + expect(Fog::Storage).to have_received(:new).once + end + end +end diff --git a/spec/services/decidim/s3_sync_service_spec.rb b/spec/services/decidim/s3_sync_service_spec.rb new file mode 100644 index 0000000000..ddc4c9d98b --- /dev/null +++ b/spec/services/decidim/s3_sync_service_spec.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Decidim::S3SyncService do + let(:temp_directory) { Dir.mktmpdir } + let(:options) { {} } + let(:instance) { described_class.new(options) } + + after do + FileUtils.rm_rf temp_directory + end + + describe ".run" do + it "executes the service" do + allow(described_class).to receive(:new).with(options).and_return instance + allow(instance).to receive(:execute).and_return "__success__" + + expect(described_class.run({})).to eq "__success__" + end + end + + describe "#default_options" do + let(:option_keys) { instance.default_options.keys } + + it "returns a hash" do + expect(instance.default_options).to be_a Hash + end + + it "has keys existing in backup configuration" do + config_keys = Rails.application.config.backup[:s3sync].keys + .map { |k| "s3_#{k}".to_sym } + + aggregate_failures do + option_keys.filter { |k| k.match?(/^s3_/) }.each do |key| + expect(config_keys).to include key + end + end + end + end + + describe "#has_local_backup_directory?" do + let(:options) { { local_backup_dir: temp_directory } } + + context "when the directory exists and is readable" do + it "returns true" do + expect(instance.has_local_backup_directory?).to be true + end + end + + context "when the directory does not exists" do + before { FileUtils.rm_rf temp_directory } + + it "returns false" do + expect(instance.has_local_backup_directory?).to be false + end + end + + context "when the directory exists but is not readable" do + before { FileUtils.chmod("ugo=wx", temp_directory) } + + it "returns false" do + expect(instance.has_local_backup_directory?).to be false + end + end + end + + describe "#subfolder" do + it "returns a memoized string" do + subfolder = instance.subfolder + + expect(subfolder).to be_a String + expect(instance.instance_variable_get(:@subfolder)).to eq subfolder + end + + context "with an option given" do + let(:options) { { subfolder: "something" } } + + it "uses the given path" do + expect(instance.subfolder).to eq "something" + end + end + + context "without an option given" do + it "generates a path" do + expect(instance.subfolder).not_to be_blank + end + end + end + + describe "#force_upload?" do + context "when option was not provided" do + it "defaults to false" do + expect(instance.force_upload?).to be false + end + end + + [true, false].each do |state| + context "when option was set to #{state}" do + let(:options) { { force_upload: state } } + + it "returns #{state}" do + expect(instance.force_upload?).to be state + end + end + end + end + + describe "#timestamp" do + it "returns a memoized string" do + timestamp = instance.timestamp + + expect(timestamp).to be_a String + expect(instance.instance_variable_get(:@timestamp)).to eq timestamp + end + end + + describe "#file_list" do + context "when no file list was provided" do + let(:options) { { local_backup_dir: temp_directory } } + + before do + Dir.chdir(temp_directory) { `touch file_1.txt file_2.txt` } + end + + it "reads from the backup directory" do + expected = [ + "#{temp_directory}/file_1.txt", + "#{temp_directory}/file_2.txt" + ] + expect(instance.file_list.sort).to eq expected + end + end + + context "when both a file list and a directory are provided" do + let(:options) do + { + local_backup_dir: temp_directory, + local_backup_files: %w(file_list_1.txt file_list_2.txt) + } + end + + before do + Dir.chdir(temp_directory) { `touch file_1.txt file_2.txt` } + end + + it "reads from the file list" do + expected = [ + "file_list_1.txt", + "file_list_2.txt" + ] + + expect(instance.file_list.sort).to eq expected + end + end + end + + describe "#service" do + it "memoizes the storage service" do + allow(Fog::Storage).to receive(:new).and_return("__success__") + + 2.times { instance.send(:service) } + expect(Fog::Storage).to have_received(:new).once + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d00ba7081b..af1e085061 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,6 +9,7 @@ RSpec.configure do |config| config.formatter = ENV.fetch("RSPEC_FORMAT", "progress").to_sym config.include EnvironmentVariablesHelper + config.include SkipIfUndefinedHelper config.before do # Initializers configs diff --git a/spec/support/skip_if_undefined_helper.rb b/spec/support/skip_if_undefined_helper.rb new file mode 100644 index 0000000000..adc421ffc7 --- /dev/null +++ b/spec/support/skip_if_undefined_helper.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module SkipIfUndefinedHelper + # Skips a test if a given class is undefined (i.e. : from another gem) + def skip_if_undefined(klass, gem) + skip "'#{gem}' gem is not present" unless klass.safe_constantize + end +end From a489a50dfb8ae348f7dc2ca60d3a0f2857bf7202 Mon Sep 17 00:00:00 2001 From: moustachu Date: Mon, 18 Dec 2023 09:38:44 +0100 Subject: [PATCH 09/13] fix: Active Storage path style for scaleway object storage (#466) --- config/storage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/storage.yml b/config/storage.yml index f902f246f0..54e2f21c1d 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -13,6 +13,7 @@ scaleway: secret_access_key: <%= Rails.application.secrets.dig(:scaleway, :token) %> region: fr-par bucket: <%= Rails.application.secrets.dig(:scaleway, :bucket_name) %> + force_path_style: true # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: From 7182e42288b07bf0386de4a0b4b9de740bcd2647 Mon Sep 17 00:00:00 2001 From: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> Date: Fri, 5 Jan 2024 11:39:24 +0100 Subject: [PATCH 10/13] feat: Add module decidim-survey_multiple_answers (#439) * feat: Add module decidim-survey_multiple_answers * fix: Use fixed branch for survey multiple answers * bump: Module survey multiple answers * fix: Add x86_64-linux support in Gemfile.lock * fix: Add missing migration for decidim-awesome * lint: lint fixes * fix: Remove not needed ActiveStorage include * ci: Lock Chromedriver to 119 --------- Co-authored-by: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Co-authored-by: AyakorK --- .github/workflows/ci_cd.yml | 4 + Gemfile | 1 + Gemfile.lock | 163 +++++++++--------- config/application.rb | 5 + config/initializers/extends.rb | 2 - ...me_vote_weights.decidim_decidim_awesome.rb | 14 ++ ...al_extra_fields.decidim_decidim_awesome.rb | 15 ++ db/schema.rb | 19 +- 8 files changed, 142 insertions(+), 81 deletions(-) create mode 100644 db/migrate/20231127192450_create_decidim_awesome_vote_weights.decidim_decidim_awesome.rb create mode 100644 db/migrate/20231127192451_create_decidim_awesome_proposal_extra_fields.decidim_decidim_awesome.rb diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 86b55a8772..3033180044 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -105,6 +105,8 @@ jobs: - run: mkdir -p ./spec/tmp/screenshots name: Create the screenshots folder - uses: nanasess/setup-chromedriver@v2 + with: + chromedriver-version: 119.0.6045.105 - run: bundle exec rake "test:run[exclude, spec/system/**/*_spec.rb, ${{ matrix.slice }}]" name: RSpec - run: ./.github/upload_coverage.sh decidim-app $GITHUB_EVENT_PATH @@ -171,6 +173,8 @@ jobs: - run: mkdir -p ./spec/tmp/screenshots name: Create the screenshots folder - uses: nanasess/setup-chromedriver@v2 + with: + chromedriver-version: 119.0.6045.105 - run: bundle exec rake "test:run[include, spec/system/**/*_spec.rb, ${{ matrix.slice }}]" name: RSpec - run: ./.github/upload_coverage.sh decidim-app $GITHUB_EVENT_PATH diff --git a/Gemfile b/Gemfile index f0868d5674..20faa4834b 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,7 @@ gem "decidim-homepage_interactive_map", git: "https://github.com/OpenSourcePolit gem "decidim-ludens", git: "https://github.com/OpenSourcePolitics/decidim-ludens.git", branch: DECIDIM_BRANCH gem "decidim-phone_authorization_handler", git: "https://github.com/OpenSourcePolitics/decidim-module_phone_authorization_handler", branch: DECIDIM_BRANCH gem "decidim-spam_detection" +gem "decidim-survey_multiple_answers", git: "https://github.com/alecslupu-pfa/decidim-module-survey_multiple_answers" gem "decidim-term_customizer", git: "https://github.com/OpenSourcePolitics/decidim-module-term_customizer.git", branch: "fix/email_with_precompile" # Omniauth gems diff --git a/Gemfile.lock b/Gemfile.lock index 17df2f5aad..5f8dfb0810 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,7 +16,7 @@ GIT GIT remote: https://github.com/OpenSourcePolitics/decidim-module-friendly_signup.git - revision: 9b8ece180ca97bf97c5d401a42a79b1dc4b8b536 + revision: 4ecc478bb8492653b7c7120103fcb3c2d1f74576 specs: decidim-friendly_signup (0.4.5) decidim-core (~> 0.27) @@ -75,6 +75,16 @@ GIT decidim-core (>= 0.27.0, < 0.28) deface (~> 1.5) +GIT + remote: https://github.com/alecslupu-pfa/decidim-module-survey_multiple_answers + revision: 65ea83227f99d0f3d6237f98334ecc914a2a5597 + specs: + decidim-survey_multiple_answers (0.26.2) + decidim-admin (>= 0.26.0, < 0.28.0) + decidim-core (>= 0.26.0, < 0.28.0) + decidim-forms (>= 0.26.0, < 0.28.0) + decidim-surveys (>= 0.26.0, < 0.28.0) + GIT remote: https://github.com/sgruhier/foundation_rails_helper.git revision: bc33600db7a2d16ce3cdc1f8369d0d7e7c4245b5 @@ -132,9 +142,9 @@ GEM activejob (6.1.7.6) activesupport (= 6.1.7.6) globalid (>= 0.3.6) - activejob-uniqueness (0.2.5) - activejob (>= 4.2, < 7.1) - redlock (>= 1.2, < 2) + activejob-uniqueness (0.3.1) + activejob (>= 4.2, < 7.2) + redlock (>= 2.0, < 3) activemodel (6.1.7.6) activesupport (= 6.1.7.6) activerecord (6.1.7.6) @@ -160,23 +170,23 @@ GEM aes_key_wrap (1.1.0) ast (2.4.2) attr_required (1.0.1) - aws-eventstream (1.2.0) - aws-partitions (1.814.0) - aws-sdk-core (3.181.0) + aws-eventstream (1.3.0) + aws-partitions (1.856.0) + aws-sdk-core (3.188.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.71.0) - aws-sdk-core (~> 3, >= 3.177.0) + aws-sdk-kms (1.73.0) + aws-sdk-core (~> 3, >= 3.188.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.134.0) - aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-s3 (1.139.0) + aws-sdk-core (~> 3, >= 3.188.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.6) - aws-sigv4 (1.6.0) + aws-sigv4 (1.7.0) aws-eventstream (~> 1, >= 1.0.2) - axe-core-api (4.7.0) + axe-core-api (4.8.0) dumb_delegator virtus axe-core-rspec (4.1.0) @@ -187,8 +197,9 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) + base64 (0.2.0) batch-loader (1.5.0) - bcrypt (3.1.19) + bcrypt (3.1.20) better_html (1.0.16) actionview (>= 4.0) activesupport (>= 4.0) @@ -199,7 +210,7 @@ GEM smart_properties bindata (2.4.15) bindex (0.8.1) - bootsnap (1.16.0) + bootsnap (1.17.0) msgpack (~> 1.2) brakeman (5.4.1) browser (2.7.1) @@ -234,7 +245,7 @@ GEM actionpack (>= 5.0) cells (>= 4.1.6, < 5.0.0) charlock_holmes (0.7.7) - chef-utils (18.2.7) + chef-utils (18.3.0) concurrent-ruby childprocess (4.1.0) climate_control (1.2.0) @@ -261,10 +272,10 @@ GEM crack (0.4.5) rexml crass (1.0.6) - css_parser (1.15.0) + css_parser (1.16.0) addressable - dalli (3.2.5) - date (3.3.3) + dalli (3.2.6) + date (3.3.4) date_validator (0.12.0) activemodel (>= 3) activesupport (>= 3) @@ -378,9 +389,10 @@ GEM decidim-debates (0.27.4) decidim-comments (= 0.27.4) decidim-core (= 0.27.4) - decidim-decidim_awesome (0.9.3) + decidim-decidim_awesome (0.10.2) decidim-admin (>= 0.26.0, < 0.28) decidim-core (>= 0.26.0, < 0.28) + deface (>= 1.5) sassc (~> 2.3) decidim-dev (0.27.4) axe-core-rspec (~> 4.1.0) @@ -473,15 +485,15 @@ GEM rainbow (>= 2.1.0) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (4.9.2) + devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-i18n (1.11.0) + devise-i18n (1.12.0) devise (>= 4.9.0) - devise_invitable (2.0.8) + devise_invitable (2.0.9) actionmailer (>= 5.0) devise (>= 4.6) diff-lcs (1.5.0) @@ -490,7 +502,7 @@ GEM nokogiri (>= 1.13.2, < 1.15.0) rubyzip (~> 2.3.0) docile (1.4.0) - doorkeeper (5.6.6) + doorkeeper (5.6.7) railties (>= 5) doorkeeper-i18n (4.0.1) dotenv (2.8.1) @@ -512,8 +524,8 @@ GEM escape_utils (1.3.0) et-orbi (1.2.7) tzinfo - excon (0.102.0) - execjs (2.8.1) + excon (0.104.0) + execjs (2.9.1) extended-markdown-filter (0.7.0) html-pipeline (~> 2.9) factory_bot (4.11.1) @@ -523,17 +535,18 @@ GEM railties (>= 3.0.0) faker (2.23.0) i18n (>= 1.8.11, < 2) - faraday (2.7.10) + faraday (2.7.12) + base64 faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) faraday-follow_redirects (0.3.0) faraday (>= 1, < 3) faraday-net_http (3.0.2) - ffi (1.15.5) + ffi (1.16.3) file_validators (3.0.0) activemodel (>= 3.2) mime-types (>= 1.0) - fog-aws (3.19.0) + fog-aws (3.21.0) fog-core (~> 2.1) fog-json (~> 1.1) fog-xml (~> 0.1) @@ -551,13 +564,13 @@ GEM fog-core nokogiri (>= 1.5.11, < 2.0.0) formatador (1.1.0) - fugit (1.8.1) + fugit (1.9.0) et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) gemoji (3.0.1) geocoder (1.8.2) - globalid (1.1.0) - activesupport (>= 5.0) + globalid (1.2.1) + activesupport (>= 6.1) graphql (1.12.24) graphql-docs (2.1.0) commonmarker (~> 0.16) @@ -592,7 +605,7 @@ GEM rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) i18n_data (0.11.0) - icalendar (2.9.0) + icalendar (2.10.0) ice_cube (~> 0.16) ice_cube (0.16.4) ice_nine (0.11.2) @@ -637,7 +650,7 @@ GEM listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - lograge (0.13.0) + lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) @@ -652,7 +665,7 @@ GEM net-smtp marcel (1.0.2) matrix (0.4.2) - mdl (0.12.0) + mdl (0.13.0) kramdown (~> 2.3) kramdown-parser-gfm (~> 1.1) mixlib-cli (~> 2.1, >= 2.1.1) @@ -661,10 +674,10 @@ GEM method_source (1.0.0) mime-types (3.5.1) mime-types-data (~> 3.2015) - mime-types-data (3.2023.0808) + mime-types-data (3.2023.1003) mini_magick (4.12.0) mini_mime (1.1.5) - minitest (5.19.0) + minitest (5.20.0) mixlib-cli (2.1.8) mixlib-config (3.0.27) tomlrb @@ -674,22 +687,18 @@ GEM multi_json (1.15.0) multi_xml (0.6.0) mustache (1.1.1) - net-imap (0.3.7) + net-imap (0.4.6) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.3.3) + net-smtp (0.4.0) net-protocol - nio4r (2.5.9) - nokogiri (1.13.4-aarch64-linux) - racc (~> 1.4) + nio4r (2.6.1) nokogiri (1.13.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.13.4-x86_64-darwin) - racc (~> 1.4) nokogiri (1.13.4-x86_64-linux) racc (~> 1.4) oauth (1.1.0) @@ -752,7 +761,7 @@ GEM parallel (1.23.0) parallel_tests (3.13.0) parallel - parser (3.2.2.3) + parser (3.2.2.4) ast (~> 2.4.1) racc pg (1.1.4) @@ -768,11 +777,11 @@ GEM actionmailer (>= 3) net-smtp premailer (~> 1.7, >= 1.7.9) - public_suffix (5.0.3) + public_suffix (5.0.4) puma (5.6.7) nio4r (~> 2.0) raabro (1.4.0) - racc (1.7.1) + racc (1.7.3) rack (2.2.8) rack-attack (6.7.0) rack (>= 1.0, < 4) @@ -786,7 +795,7 @@ GEM rack (>= 2.1.0) rack-protection (3.1.0) rack (~> 2.2, >= 2.2.4) - rack-proxy (0.7.6) + rack-proxy (0.7.7) rack rack-test (2.1.0) rack (>= 1.3) @@ -825,7 +834,7 @@ GEM rake (>= 12.2) thor (~> 1.0) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) ransack (2.4.2) activerecord (>= 5.2.4) activesupport (>= 5.2.4) @@ -835,12 +844,14 @@ GEM ffi (~> 1.0) redcarpet (3.6.0) redis (4.8.1) - redlock (1.3.2) - redis (>= 3.0.0, < 6.0) - regexp_parser (2.8.1) + redis-client (0.18.0) + connection_pool + redlock (2.0.6) + redis-client (>= 0.14.1, < 1.0.0) + regexp_parser (2.8.2) request_store (1.5.1) rack (>= 1.4) - responders (3.1.0) + responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) rexml (3.2.6) @@ -887,7 +898,7 @@ GEM rubocop-ast (>= 1.17.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.29.0) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) rubocop-faker (1.1.0) faker (>= 2.12.0) @@ -899,7 +910,7 @@ GEM rubocop-rspec (2.11.1) rubocop (~> 1.19) ruby-progressbar (1.13.0) - ruby-vips (2.1.4) + ruby-vips (2.2.0) ffi (~> 1.12) ruby2_keywords (0.0.5) rubyXL (3.4.25) @@ -923,16 +934,16 @@ GEM semantic_range (3.0.0) sendgrid-ruby (6.6.2) ruby_http_client (~> 3.4) - sentry-rails (5.10.0) + sentry-rails (5.13.0) railties (>= 5.0) - sentry-ruby (~> 5.10.0) - sentry-ruby (5.10.0) + sentry-ruby (~> 5.13.0) + sentry-ruby (5.13.0) concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.10.0) - sentry-ruby (~> 5.10.0) + sentry-sidekiq (5.13.0) + sentry-ruby (~> 5.13.0) sidekiq (>= 3.0) seven_zip_ruby (1.3.0) - sidekiq (6.5.9) + sidekiq (6.5.12) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) @@ -940,7 +951,7 @@ GEM rufus-scheduler (~> 3.2) sidekiq (>= 6, < 8) tilt (>= 1.4.0) - sidekiq_alive (2.2.3) + sidekiq_alive (2.3.1) rack (< 3) sidekiq (>= 5, < 8) webrick (>= 1, < 2) @@ -966,32 +977,32 @@ GEM spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) spring (>= 1.2, < 3.0) - sprockets (4.2.0) + sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - ssrf_filter (1.1.1) + ssrf_filter (1.1.2) swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) httpclient (>= 2.4) - sys-filesystem (1.4.3) + sys-filesystem (1.4.4) ffi (~> 1.1) - temple (0.10.2) + temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - thor (1.2.2) + thor (1.3.0) thread_safe (0.3.6) - tilt (2.2.0) - timeout (0.4.0) + tilt (2.3.0) + timeout (0.4.1) tomlrb (2.0.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (2.4.2) + unicode-display_width (2.5.0) unicode_utils (1.4.0) valid_email2 (2.3.1) activemodel (>= 3.2) @@ -1043,22 +1054,17 @@ GEM websocket-extensions (0.1.5) wicked (1.4.0) railties (>= 3.0.7) - wicked_pdf (2.6.3) + wicked_pdf (2.7.0) activesupport wisper (2.0.1) wisper-rspec (1.1.0) wkhtmltopdf-binary (0.12.6.6) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.11) + zeitwerk (2.6.12) PLATFORMS - aarch64-linux - arm64-darwin-21 arm64-darwin-22 - x86_64-darwin-20 - x86_64-darwin-21 - x86_64-darwin-22 x86_64-linux DEPENDENCIES @@ -1082,6 +1088,7 @@ DEPENDENCIES decidim-ludens! decidim-phone_authorization_handler! decidim-spam_detection + decidim-survey_multiple_answers! decidim-templates (~> 0.27.0) decidim-term_customizer! deepl-rb diff --git a/config/application.rb b/config/application.rb index c08cae0cf7..8e5d323de9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -38,6 +38,11 @@ class Application < Rails::Application # -- all .rb files in that directory are automatically loaded after loading # the framework and any gems in your application. + config.to_prepare do + require "extends/helpers/decidim/forms/application_helper_extends" + require "extends/cells/decidim/forms/step_navigation_cell_extends" + end + config.after_initialize do require "extends/controllers/decidim/devise/sessions_controller_extends" require "extends/controllers/decidim/editor_images_controller_extends" diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index b66943c838..c39712135d 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true require "extends/controllers/decidim/devise/account_controller_extends" -require "extends/cells/decidim/forms/step_navigation_cell_extends" require "extends/cells/decidim/content_blocks/hero_cell_extends" -require "extends/helpers/decidim/forms/application_helper_extends" require "extends/uploaders/decidim/application_uploader_extends" diff --git a/db/migrate/20231127192450_create_decidim_awesome_vote_weights.decidim_decidim_awesome.rb b/db/migrate/20231127192450_create_decidim_awesome_vote_weights.decidim_decidim_awesome.rb new file mode 100644 index 0000000000..64482b4ed9 --- /dev/null +++ b/db/migrate/20231127192450_create_decidim_awesome_vote_weights.decidim_decidim_awesome.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +# This migration comes from decidim_decidim_awesome (originally 20231006113837) + +class CreateDecidimAwesomeVoteWeights < ActiveRecord::Migration[6.0] + def change + create_table :decidim_awesome_vote_weights do |t| + # this might be polymorphic in the future (if other types of votes are supported) + t.references :proposal_vote, null: false, index: { name: "decidim_awesome_proposals_weights_vote" } + + t.integer :weight, null: false, default: 1 + t.timestamps + end + end +end diff --git a/db/migrate/20231127192451_create_decidim_awesome_proposal_extra_fields.decidim_decidim_awesome.rb b/db/migrate/20231127192451_create_decidim_awesome_proposal_extra_fields.decidim_decidim_awesome.rb new file mode 100644 index 0000000000..781fd3f069 --- /dev/null +++ b/db/migrate/20231127192451_create_decidim_awesome_proposal_extra_fields.decidim_decidim_awesome.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true +# This migration comes from decidim_decidim_awesome (originally 20231006113841) + +class CreateDecidimAwesomeProposalExtraFields < ActiveRecord::Migration[6.0] + def change + create_table :decidim_awesome_proposal_extra_fields do |t| + # this might be polymorphic in the future (if other types of votes are supported) + t.references :decidim_proposal, null: false, index: { name: "decidim_awesome_extra_fields_on_proposal" } + + t.jsonb :vote_weight_totals + t.integer :weight_total, default: 0 + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 386961c075..fa473378af 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: 2023_08_31_093832) do +ActiveRecord::Schema.define(version: 2023_11_27_192451) do # These are extensions that must be enabled in order to support this database enable_extension "ltree" @@ -323,6 +323,23 @@ t.index ["decidim_organization_id"], name: "decidim_awesome_editor_images_constraint_organization" end + create_table "decidim_awesome_proposal_extra_fields", force: :cascade do |t| + t.bigint "decidim_proposal_id", null: false + t.jsonb "vote_weight_totals" + t.integer "weight_total", default: 0 + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["decidim_proposal_id"], name: "decidim_awesome_extra_fields_on_proposal" + end + + create_table "decidim_awesome_vote_weights", force: :cascade do |t| + t.bigint "proposal_vote_id", null: false + t.integer "weight", default: 1, null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["proposal_vote_id"], name: "decidim_awesome_proposals_weights_vote" + end + create_table "decidim_blogs_posts", id: :serial, force: :cascade do |t| t.jsonb "title" t.jsonb "body" From 66df9c662d51ffc365c952fdf00902f80f0706f4 Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:16:17 +0100 Subject: [PATCH 11/13] fix: Compatibility between committee promotion and initiatives creation (#470) * fix: Change the behavior of the committee requests to make it intuitive * fix: test addition * fix: Redirect to login page if logged out on the page * fix: Improve code coverage * fix: Add some more tests to try to improve code coverage --- config/application.rb | 1 + .../committee_requests_controller_extends.rb | 26 ++ .../committee_requests_controller_spec.rb | 226 ++++++++++++++++++ 3 files changed, 253 insertions(+) create mode 100644 lib/extends/controllers/decidim/initiatives/committee_requests_controller_extends.rb create mode 100644 spec/controllers/decidim/initiatives/committee_requests_controller_spec.rb diff --git a/config/application.rb b/config/application.rb index 8e5d323de9..312fa370d7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -49,6 +49,7 @@ class Application < Rails::Application require "extends/services/decidim/iframe_disabler_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" 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/lib/extends/controllers/decidim/initiatives/committee_requests_controller_extends.rb b/lib/extends/controllers/decidim/initiatives/committee_requests_controller_extends.rb new file mode 100644 index 0000000000..960295a967 --- /dev/null +++ b/lib/extends/controllers/decidim/initiatives/committee_requests_controller_extends.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module CommitteeRequestsControllerExtends + def new + return if authorized?(current_user) + + if current_user.nil? + redirect_to decidim.new_user_session_path + else + authorization_method = Decidim::Verifications::Adapter.from_element(current_initiative.document_number_authorization_handler) + redirect_url = new_initiative_committee_request_path(current_initiative) + redirect_to authorization_method.root_path(redirect_url: redirect_url) + end + end + + private + + def authorized?(user) + authorization = current_initiative.document_number_authorization_handler + Decidim::Authorization.exists?(user: user, name: authorization) + end +end + +Decidim::Initiatives::CommitteeRequestsController.class_eval do + prepend(CommitteeRequestsControllerExtends) +end diff --git a/spec/controllers/decidim/initiatives/committee_requests_controller_spec.rb b/spec/controllers/decidim/initiatives/committee_requests_controller_spec.rb new file mode 100644 index 0000000000..2ef1af7b69 --- /dev/null +++ b/spec/controllers/decidim/initiatives/committee_requests_controller_spec.rb @@ -0,0 +1,226 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Initiatives + describe CommitteeRequestsController, type: :controller do + routes { Decidim::Initiatives::Engine.routes } + + let(:organization) { create(:organization) } + let!(:initiative) { create(:initiative, :created, organization: organization) } + let(:admin_user) { create(:user, :admin, :confirmed, organization: organization) } + let(:user) { create(:user, :confirmed, organization: organization) } + + before do + request.env["decidim.current_organization"] = organization + end + + context "when GET new" do + let(:current_user) { create(:user, :confirmed, organization: organization) } + let(:authorization_handler) { "dummy_authorization_handler" } + let(:committee_request_path) { "/initiatives/#{initiative.id}/committee_requests/new" } + + before do + allow(controller).to receive(:current_initiative).and_return(initiative) + allow(controller).to receive(:current_user).and_return(current_user) + allow(controller).to receive(:authorized?).and_return(authorized) + allow(initiative).to receive(:document_number_authorization_handler).and_return(authorization_handler) + end + + context "when not authorized" do + let(:authorized) { false } + + it "redirects to authorization root path" do + allow(controller).to receive(:authorized?).with(current_user).and_return(false) + allow(controller).to receive(:new_initiative_committee_request_path).with(initiative).and_return(committee_request_path) + + get :new, params: { initiative_slug: initiative.slug } + + expect(response).to have_http_status(:found) + end + end + + context "when not logged in" do + let(:current_user) { nil } + let(:authorized) { false } + + it "redirects to login page" do + allow(controller).to receive(:new_initiative_committee_request_path).with(initiative).and_return(committee_request_path) + + get :new, params: { initiative_slug: initiative.slug } + + expect(response).to have_http_status(:found) + expect(URI.parse(response.location).path).to eq("/users/sign_in") + end + end + + context "when authorized" do + let(:authorized) { true } + + it "does not redirect" do + allow(controller).to receive(:authorized?).with(current_user).and_return(true) + + get :new, params: { initiative_slug: initiative.slug } + + expect(response).to have_http_status(:ok) + end + end + end + + context "when authorized? is called" do + let(:current_user) { create(:user, :confirmed, organization: organization) } + let(:authorization_handler) { "dummy_authorization_handler" } + + before do + allow(controller).to receive(:current_initiative).and_return(initiative) + allow(controller).to receive(:current_user).and_return(current_user) + allow(initiative).to receive(:document_number_authorization_handler).and_return(authorization_handler) + end + + context "when authorized" do + it "returns true" do + allow(controller).to receive(:authorized?).with(current_user).and_return(true) + + result = controller.send(:authorized?, current_user) + + expect(result).to be(true) + end + end + + context "when not authorized" do + it "returns false" do + allow(controller).to receive(:authorized?).with(current_user).and_return(false) + + result = controller.send(:authorized?, current_user) + + expect(result).to be(false) + end + end + end + + context "when GET spawn" do + let(:user) { create(:user, :confirmed, organization: organization) } + + before do + create(:authorization, user: user) + sign_in user, scope: :user + end + + context "and created initiative" do + it "Membership request is created" do + expect do + get :spawn, params: { initiative_slug: initiative.slug } + end.to change(InitiativesCommitteeMember, :count).by(1) + end + + it "Duplicated requests finish with an error" do + expect do + get :spawn, params: { initiative_slug: initiative.slug } + end.to change(InitiativesCommitteeMember, :count).by(1) + + expect do + get :spawn, params: { initiative_slug: initiative.slug } + end.not_to change(InitiativesCommitteeMember, :count) + end + end + + context "and published initiative" do + let!(:published_initiative) { create(:initiative, :published, organization: organization) } + + it "Membership request is not created" do + expect do + get :spawn, params: { initiative_slug: published_initiative.slug } + end.not_to change(InitiativesCommitteeMember, :count) + end + end + end + + context "when GET approve" do + let(:membership_request) { create(:initiatives_committee_member, initiative: initiative, state: "requested") } + + context "and Owner" do + before do + sign_in initiative.author, scope: :user + end + + it "request gets approved" do + get :approve, params: { initiative_slug: membership_request.initiative.to_param, id: membership_request.to_param } + membership_request.reload + expect(membership_request).to be_accepted + end + end + + context "and other users" do + let(:user) { create(:user, :confirmed, organization: organization) } + + before do + create(:authorization, user: user) + sign_in user, scope: :user + end + + it "Action is denied" do + get :approve, params: { initiative_slug: membership_request.initiative.to_param, id: membership_request.to_param } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and Admin" do + before do + sign_in admin_user, scope: :user + end + + it "request gets approved" do + get :approve, params: { initiative_slug: membership_request.initiative.to_param, id: membership_request.to_param } + membership_request.reload + expect(membership_request).to be_accepted + end + end + end + + context "when DELETE revoke" do + let(:membership_request) { create(:initiatives_committee_member, initiative: initiative, state: "requested") } + + context "and Owner" do + before do + sign_in initiative.author, scope: :user + end + + it "request gets approved" do + delete :revoke, params: { initiative_slug: membership_request.initiative.to_param, id: membership_request.to_param } + membership_request.reload + expect(membership_request).to be_rejected + end + end + + context "and Other users" do + let(:user) { create(:user, :confirmed, organization: organization) } + + before do + create(:authorization, user: user) + sign_in user, scope: :user + end + + it "Action is denied" do + delete :revoke, params: { initiative_slug: membership_request.initiative.to_param, id: membership_request.to_param } + expect(flash[:alert]).not_to be_empty + expect(response).to have_http_status(:found) + end + end + + context "and Admin" do + before do + sign_in admin_user, scope: :user + end + + it "request gets approved" do + delete :revoke, params: { initiative_slug: membership_request.initiative.to_param, id: membership_request.to_param } + membership_request.reload + expect(membership_request).to be_rejected + end + end + end + end + end +end From f91f54361d3ff81e1dd5c401fbb8e0dc7da5bfdb Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Thu, 11 Jan 2024 16:47:27 +0100 Subject: [PATCH 12/13] [Fix] - Single Initiative Type Blocked the edit form (#471) * fix: Fix the issue of the single initiative not retrieving infos and add tests * fix: Add missing locales --- .../admin/initiatives/_form.html.erb | 133 ++++++++++++++++++ config/locales/en.yml | 8 ++ config/locales/fr.yml | 8 ++ ...nitiative_administration_shared_context.rb | 20 +++ spec/system/admin/update_initiative_spec.rb | 124 ++++++++++++++++ 5 files changed, 293 insertions(+) create mode 100644 app/views/decidim/initiatives/admin/initiatives/_form.html.erb create mode 100644 spec/shared/initiative_administration_shared_context.rb create mode 100644 spec/system/admin/update_initiative_spec.rb diff --git a/app/views/decidim/initiatives/admin/initiatives/_form.html.erb b/app/views/decidim/initiatives/admin/initiatives/_form.html.erb new file mode 100644 index 0000000000..52d016b9db --- /dev/null +++ b/app/views/decidim/initiatives/admin/initiatives/_form.html.erb @@ -0,0 +1,133 @@ +
+
+

<%= t ".title" %>

+
+ +
+
+ <%= form.translated :text_field, :title, autofocus: true, disabled: !allowed_to?(:update, :initiative, initiative: current_initiative) %> +
+ +
+ <%= form.translated :editor, :description, lines: 8, disabled: !allowed_to?(:update, :initiative, initiative: current_initiative) %> +
+ +
+
+ <%= form.text_field :hashtag, disabled: !allowed_to?(:update, :initiative, initiative: current_initiative) %> +
+
+
+
+ +
+
+

<%= t ".settings" %>

+
+ +
+
+
+ <%= form.select :state, + Decidim::Initiative.states.keys.map { |state| [I18n.t(state, scope: "decidim.initiatives.admin_states"), state] }, + {}, + { disabled: !@form.state_updatable? } %> +
+
+ +
+
+ <% unless single_initiative_type? %> + <%= form.select :type_id, + initiative_type_options, + {}, + { + disabled: !@form.signature_type_updatable?, + "data-scope-selector": "initiative_decidim_scope_id", + "data-scope-id": form.object.decidim_scope_id.to_s, + "data-scope-search-url": decidim_initiatives.initiative_type_scopes_search_url, + "data-signature-types-selector": "initiative_signature_type", + "data-signature-type": current_initiative.signature_type, + "data-signature-types-search-url": decidim_initiatives.initiative_type_signature_types_search_url + } %> +
+ <% else %> + <%= form.hidden_field :type_id, + { + disabled: !@form.signature_type_updatable?, + "data-scope-selector": "initiative_decidim_scope_id", + "data-scope-id": form.object.decidim_scope_id.to_s, + "data-scope-search-url": decidim_initiatives.initiative_type_scopes_search_url, + "data-signature-types-selector": "initiative_signature_type", + "data-signature-type": current_initiative.signature_type, + "data-signature-types-search-url": decidim_initiatives.initiative_type_signature_types_search_url + } %> + <% end %> +
+ <%= form.select :decidim_scope_id, [], {}, { disabled: !@form.signature_type_updatable? } %> +
+
+ + <% if current_initiative.published? && current_user.admin? %> +
+
+ <%= form.date_field :signature_start_date %> +
+ +
+ <%= form.date_field :signature_end_date %> +
+
+ <% end %> + + <% if can_edit_custom_signature_end_date?(current_initiative) %> +
+ <%= form.date_field :signature_end_date, disabled: !allowed_to?(:update, :initiative, initiative: current_initiative) %> +
+ <% end %> + + <% if current_initiative.area_enabled? %> +
+ <%= form.areas_select :area_id, + areas_for_select(current_organization), + { + selected: current_initiative.decidim_area_id, + include_blank: current_initiative.decidim_area_id.blank? || current_initiative.created? + }, + disabled: !@form.area_updatable? %> +
+ <% end %> + +
+
+ <%= form.select :signature_type, [], {}, { disabled: !@form.signature_type_updatable? } %> +
+
+ + <% if current_initiative.accepts_offline_votes? && current_user.admin? %> +
+
+ <% @form.offline_votes.each do |scope_id, (votes, scope_name)| %> + <%= label_tag "initiative_offline_votes_#{scope_id}", t("activemodel.attributes.initiative.offline_votes_for_scope", scope_name: translated_attribute(scope_name)) %> + <%= number_field_tag "initiative[offline_votes][#{scope_id}]", votes, min: 0, id: "initiative_offline_votes_#{scope_id}" %> + <% end %> +
+
+ <% end %> +
+
+
+
+

<%= t ".attachments" %>

+
+ +
+
+ <% if allowed_to?(:read, :attachment, initiative: current_participatory_space) %> + <%= render partial: "initiative_attachments", locals: { current_initiative: current_initiative, current_participatory_space: current_participatory_space } %> + <% end %> +
+
+
+ +<%= javascript_pack_tag "decidim_initiatives_admin" %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 543840c3ce..58a67b4173 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,6 +2,8 @@ en: activemodel: attributes: + initiative: + offline_votes_for_scope: In-person signatures for %{scope_name} osp_authorization_handler: birthday: Birthday document_number: Unique number @@ -67,6 +69,12 @@ en: email_subject: Failed verification attempt against a managed participant notification_title: The participant %{resource_title} has tried to verify themself with the data of the managed participant %{managed_user_name}. initiatives: + admin: + initiatives: + form: + attachments: Attachments + settings: Settings + title: General information pages: home: highlighted_initiatives: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 93f277f432..b325f8ccb6 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -2,6 +2,8 @@ fr: activemodel: attributes: + initiative: + offline_votes_for_scope: Signatures en personne pour %{scope_name} osp_authorization_handler: birthday: Date de naissance document_number: Numéro unique @@ -69,6 +71,12 @@ fr: email_subject: Un utilisateur a tenté de se faire vérifier avec les données d'un utilisateur représenté notification_title: Le participant %{resource_title} a tenté de se faire vérifier avec les données de l'utilisateur représenté %{managed_user_name}. initiatives: + admin: + initiatives: + form: + attachments: Pièces jointes + settings: Paramètres + title: Informations générales pages: home: highlighted_initiatives: diff --git a/spec/shared/initiative_administration_shared_context.rb b/spec/shared/initiative_administration_shared_context.rb new file mode 100644 index 0000000000..abb44ddbf1 --- /dev/null +++ b/spec/shared/initiative_administration_shared_context.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +shared_context "when admins initiative" do + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization: organization) } + let(:author) { create(:user, :confirmed, organization: organization) } + let(:other_initiatives_type) { create(:initiatives_type, organization: organization, signature_type: "any") } + let!(:other_initiatives_type_scope) { create(:initiatives_type_scope, type: other_initiatives_type) } + + let(:initiative_type) { create(:initiatives_type, organization: organization) } + let(:initiative_scope) { create(:initiatives_type_scope, type: initiative_type) } + let!(:initiative) { create(:initiative, organization: organization, scoped_type: initiative_scope, author: author) } + + let(:image1_filename) { "city.jpeg" } + let(:image1_path) { Decidim::Dev.asset(image1_filename) } + let(:image2_filename) { "city2.jpeg" } + let(:image2_path) { Decidim::Dev.asset(image2_filename) } + let(:image3_filename) { "city3.jpeg" } + let(:image3_path) { Decidim::Dev.asset(image3_filename) } +end diff --git a/spec/system/admin/update_initiative_spec.rb b/spec/system/admin/update_initiative_spec.rb new file mode 100644 index 0000000000..ff65446f1b --- /dev/null +++ b/spec/system/admin/update_initiative_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "User prints the initiative", type: :system do + include_context "when admins initiative" + + def submit_and_validate + find("*[type=submit]").click + + within ".callout-wrapper" do + expect(page).to have_content("successfully") + end + end + + context "when initiative update" do + context "and user is admin" do + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin_initiatives.initiatives_path + end + + it "Updates published initiative data" do + page.find(".action-icon--edit").click + within ".edit_initiative" do + fill_in :initiative_hashtag, with: "#hashtag" + end + submit_and_validate + end + + context "when initiative is in created state" do + before do + initiative.created! + end + + it "updates type, scope and signature type" do + page.find(".action-icon--edit").click + within ".edit_initiative" do + select translated(other_initiatives_type.title), from: "initiative_type_id" + select translated(other_initiatives_type_scope.scope.name), from: "initiative_decidim_scope_id" + select "In-person", from: "initiative_signature_type" + end + submit_and_validate + end + + it "displays initiative attachments" do + page.find(".action-icon--edit").click + expect(page).to have_link("Edit") + expect(page).to have_link("New") + end + end + + context "when initiative is in validating state" do + before do + initiative.validating! + end + + it "updates type, scope and signature type" do + page.find(".action-icon--edit").click + within ".edit_initiative" do + select translated(other_initiatives_type.title), from: "initiative_type_id" + select translated(other_initiatives_type_scope.scope.name), from: "initiative_decidim_scope_id" + select "In-person", from: "initiative_signature_type" + end + submit_and_validate + end + + it "displays initiative attachments" do + page.find(".action-icon--edit").click + expect(page).to have_link("Edit") + expect(page).to have_link("New") + end + end + + context "when initiative is in accepted state" do + before do + initiative.accepted! + end + + it "update of type, scope and signature type are disabled" do + page.find(".action-icon--edit").click + + within ".edit_initiative" do + expect(page).to have_css("#initiative_type_id[disabled]") + expect(page).to have_css("#initiative_decidim_scope_id[disabled]") + expect(page).to have_css("#initiative_signature_type[disabled]") + end + end + + it "displays initiative attachments" do + page.find(".action-icon--edit").click + expect(page).to have_link("Edit") + expect(page).to have_link("New") + end + end + + context "when there is a single initiative type" do + let!(:other_initiatives_type) { nil } + let!(:other_initiatives_type_scope) { nil } + + before do + initiative.created! + end + + it "update of type, scope and signature type are disabled" do + page.find(".action-icon--edit").click + + within ".edit_initiative" do + expect(page).not_to have_css("label[for='initiative_type_id']") + expect(page).not_to have_css("#initiative_type_id") + + expect(page).to have_css("label[for='initiative_decidim_scope_id']") + expect(page).to have_css("#initiative_decidim_scope_id") + expect(page).to have_css("option[value='#{initiative_scope.id}'][selected='selected']") + expect(page).to have_css("label[for='initiative_signature_type']") + expect(page).to have_css("#initiative_signature_type") + expect(page).to have_css("option[value='#{initiative.signature_type}'][selected='selected']") + end + end + end + end + end +end From 052c5d52152a0c8d3b37584491b57bcd91143f7e Mon Sep 17 00:00:00 2001 From: Quentin Champenois <26109239+Quentinchampenois@users.noreply.github.com> Date: Thu, 11 Jan 2024 18:04:27 +0100 Subject: [PATCH 13/13] feat: Add configurable session store with ActiveRecord (#472) --- .env-example | 4 +++- Gemfile | 1 + Gemfile.lock | 9 +++++++++ config/application.rb | 7 +++++++ db/migrate/20240109144022_add_sessions_table.rb | 16 ++++++++++++++++ db/schema.rb | 11 ++++++++++- docker-compose.local.yml | 2 ++ 7 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20240109144022_add_sessions_table.rb diff --git a/.env-example b/.env-example index 42e2541580..aa172d1031 100644 --- a/.env-example +++ b/.env-example @@ -72,4 +72,6 @@ DECIDIM_ADMIN_PASSWORD_STRONG="false" # PUMA_MIN_THREADS=5 # PUMA_MAX_THREADS=5 # PUMA_WORKERS=0 -# PUMA_PRELOAD_APP=false \ No newline at end of file +# PUMA_PRELOAD_APP=false + +# RAILS_SESSION_STORE=active_record \ No newline at end of file diff --git a/Gemfile b/Gemfile index 20faa4834b..e40e01fa04 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,7 @@ gem "omniauth-publik", git: "https://github.com/OpenSourcePolitics/omniauth-publ # Default gem "activejob-uniqueness", require: "active_job/uniqueness/sidekiq_patch" +gem "activerecord-session_store" gem "aws-sdk-s3", require: false gem "bootsnap", "~> 1.4" gem "deepl-rb", require: "deepl" diff --git a/Gemfile.lock b/Gemfile.lock index 5f8dfb0810..8500328eb3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,6 +150,13 @@ GEM activerecord (6.1.7.6) activemodel (= 6.1.7.6) activesupport (= 6.1.7.6) + activerecord-session_store (2.1.0) + actionpack (>= 6.1) + activerecord (>= 6.1) + cgi (>= 0.3.6) + multi_json (~> 1.11, >= 1.11.2) + rack (>= 2.0.8, < 4) + railties (>= 6.1) activestorage (6.1.7.6) actionpack (= 6.1.7.6) activejob (= 6.1.7.6) @@ -244,6 +251,7 @@ GEM cells-rails (0.1.5) actionpack (>= 5.0) cells (>= 4.1.6, < 5.0.0) + cgi (0.4.1) charlock_holmes (0.7.7) chef-utils (18.3.0) concurrent-ruby @@ -1069,6 +1077,7 @@ PLATFORMS DEPENDENCIES activejob-uniqueness + activerecord-session_store aws-sdk-s3 bootsnap (~> 1.4) brakeman (~> 5.1) diff --git a/config/application.rb b/config/application.rb index 312fa370d7..0ba9a737a4 100644 --- a/config/application.rb +++ b/config/application.rb @@ -55,5 +55,12 @@ class Application < Rails::Application config.initial_query = "{\n deployment {\n version\n branch\n remote\n upToDate\n currentCommit\n latestCommit\n locallyModified\n }\n}".html_safe end end + + if ENV.fetch("RAILS_SESSION_STORE", "") == "active_record" + initializer "session cookie domain", after: "Expire sessions" do + Rails.application.config.session_store :active_record_store, key: "_decidim_session", expire_after: Decidim.config.expire_session_after + ActiveRecord::SessionStore::Session.serializer = :hybrid + end + end end end diff --git a/db/migrate/20240109144022_add_sessions_table.rb b/db/migrate/20240109144022_add_sessions_table.rb new file mode 100644 index 0000000000..fced761a9c --- /dev/null +++ b/db/migrate/20240109144022_add_sessions_table.rb @@ -0,0 +1,16 @@ +class AddSessionsTable < ActiveRecord::Migration[6.1] + def up + create_table :sessions do |t| + t.string :session_id, null: false + t.text :data + t.timestamps + end + + add_index :sessions, :session_id, unique: true + add_index :sessions, :updated_at + end + + def down + drop_table :sessions + end +end diff --git a/db/schema.rb b/db/schema.rb index fa473378af..817e909a49 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: 2023_11_27_192451) do +ActiveRecord::Schema.define(version: 2024_01_09_144022) do # These are extensions that must be enabled in order to support this database enable_extension "ltree" @@ -2052,6 +2052,15 @@ t.index ["redirect_rule_id"], name: "index_request_environment_rules_on_redirect_rule_id" end + create_table "sessions", force: :cascade do |t| + t.string "session_id", null: false + t.text "data" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["session_id"], name: "index_sessions_on_session_id", unique: true + t.index ["updated_at"], name: "index_sessions_on_updated_at" + end + create_table "versions", force: :cascade do |t| t.string "item_type", null: false t.integer "item_id", null: false diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 65ee35b4a4..30beb45e4e 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -40,6 +40,7 @@ services: - PUMA_MAX_THREADS=5 - PUMA_WORKERS=4 - PUMA_PRELOAD_APP=true + - RAILS_SESSION_STORE=active_record depends_on: - app volumes: @@ -70,6 +71,7 @@ services: - PUMA_MAX_THREADS=5 - PUMA_WORKERS=4 - PUMA_PRELOAD_APP=true + - RAILS_SESSION_STORE=active_record volumes: - shared-volume:/app ports: