diff --git a/.env-example b/.env-example index cc4ee09c86..aa172d1031 100644 --- a/.env-example +++ b/.env-example @@ -67,4 +67,11 @@ 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 + +# RAILS_SESSION_STORE=active_record \ No newline at end of file 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/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..955752640b --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,27 @@ +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '0 8 * * *' + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'Stale issue message' + stale-pr-message: 'Stale pull request message' + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' diff --git a/.gitignore b/.gitignore index 95159cb151..a146a38cae 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,6 @@ yarn-debug.log* coverage/ public/sw.js* app/compiled_views/ +certificate-https-local/ + +.DS_Store \ No newline at end of file diff --git a/Dockerfile.local b/Dockerfile.local new file mode 100644 index 0000000000..5f4deb115d --- /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 assets:precompile && \ + bundle exec rails deface: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 1891d1e732..158d206b8e 100644 --- a/Gemfile +++ b/Gemfile @@ -18,14 +18,14 @@ gem "decidim-cache_cleaner" gem "decidim-decidim_awesome" gem "decidim-extended_socio_demographic_authorization_handler", git: "https://github.com/OpenSourcePolitics/decidim-module-extended_socio_demographic_authorization_handler.git", branch: DECIDIM_BRANCH -gem "decidim-extra_user_fields", git: "https://github.com/PopulateTools/decidim-module-extra_user_fields.git", branch: "extra-fields-0-27" +gem "decidim-extra_user_fields", git: "https://github.com/PopulateTools/decidim-module-extra_user_fields.git", branch: "release/0.27-stable" gem "decidim-friendly_signup", git: "https://github.com/OpenSourcePolitics/decidim-module-friendly_signup.git" -# TODO: Bump to 0.27.0 when released -# gem "decidim-gallery" +gem "decidim-gallery", git: "https://github.com/OpenSourcePolitics/decidim-module-gallery.git", branch: "fix/nokogiri_deps" gem "decidim-homepage_interactive_map", git: "https://github.com/OpenSourcePolitics/decidim-module-homepage_interactive_map.git", branch: DECIDIM_BRANCH 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 @@ -34,6 +34,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" @@ -42,6 +43,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 +51,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/Gemfile.lock b/Gemfile.lock index f0a6e81b41..6629bac9bd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,11 +16,20 @@ GIT GIT remote: https://github.com/OpenSourcePolitics/decidim-module-friendly_signup.git - revision: 9b8ece180ca97bf97c5d401a42a79b1dc4b8b536 + revision: 1a054faf964cc6ed07f1bec47cb92f0083a5bcb4 specs: decidim-friendly_signup (0.4.5) decidim-core (~> 0.27) +GIT + remote: https://github.com/OpenSourcePolitics/decidim-module-gallery.git + revision: 726fab33984c3adeec30cf90d0e1b4ad3881787d + branch: fix/nokogiri_deps + specs: + decidim-gallery (0.26.0) + decidim-admin (>= 0.26.0, < 0.28.0) + decidim-core (>= 0.26.0, < 0.28.0) + GIT remote: https://github.com/OpenSourcePolitics/decidim-module-homepage_interactive_map.git revision: dd685166fdf953a11bd6a9e0dac56feca3bd0708 @@ -35,7 +44,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) @@ -67,14 +76,24 @@ GIT GIT remote: https://github.com/PopulateTools/decidim-module-extra_user_fields.git - revision: 0fee03bf94dce989d36153cfa2b973c388172fc3 - branch: extra-fields-0-27 + revision: ef262d6619ef254de1379278f56c4c6af789e54c + branch: release/0.27-stable specs: decidim-extra_user_fields (0.27.2) country_select (~> 4.0) 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 @@ -88,6 +107,7 @@ GIT GEM remote: https://rubygems.org/ specs: + abbrev (0.1.2) actioncable (6.1.7.6) actionpack (= 6.1.7.6) activesupport (= 6.1.7.6) @@ -132,14 +152,21 @@ 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) 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) @@ -155,28 +182,28 @@ GEM zeitwerk (~> 2.3) acts_as_list (0.9.19) activerecord (>= 3.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) 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, >= 1.0.2) + attr_required (1.0.2) + aws-eventstream (1.3.0) + aws-partitions (1.882.0) + aws-sdk-core (3.190.3) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.71.0) - aws-sdk-core (~> 3, >= 3.177.0) + aws-sdk-kms (1.76.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.142.0) + aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.6.0) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) - axe-core-api (4.7.0) + axe-core-api (4.8.1) dumb_delegator virtus axe-core-rspec (4.1.0) @@ -187,8 +214,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 +227,7 @@ GEM smart_properties bindata (2.4.15) bindex (0.8.1) - bootsnap (1.16.0) + bootsnap (1.17.1) msgpack (~> 1.2) brakeman (5.4.1) browser (2.7.1) @@ -214,7 +242,7 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - carrierwave (2.2.4) + carrierwave (2.2.5) activemodel (>= 5.0.0) activesupport (>= 5.0.0) addressable (~> 2.6) @@ -233,8 +261,9 @@ 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.2.7) + chef-utils (18.3.0) concurrent-ruby childprocess (4.1.0) climate_control (1.2.0) @@ -249,7 +278,7 @@ GEM coffee-script-source (1.12.2) colorize (0.8.1) commonmarker (0.23.10) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) connection_pool (2.4.1) countries (3.1.0) i18n_data (~> 0.11.0) @@ -261,10 +290,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 +407,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 +503,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 +520,7 @@ GEM nokogiri (>= 1.13.2, < 1.15.0) rubyzip (~> 2.3.0) docile (1.4.0) - doorkeeper (5.6.6) + doorkeeper (5.6.8) railties (>= 5) doorkeeper-i18n (4.0.1) dotenv (2.8.1) @@ -512,8 +542,8 @@ GEM escape_utils (1.3.0) et-orbi (1.2.7) tzinfo - excon (0.102.0) - execjs (2.8.1) + excon (0.109.0) + execjs (2.9.1) extended-markdown-filter (0.7.0) html-pipeline (~> 2.9) factory_bot (4.11.1) @@ -523,21 +553,21 @@ GEM railties (>= 3.0.0) faker (2.23.0) i18n (>= 1.8.11, < 2) - faraday (2.7.10) - faraday-net_http (>= 2.0, < 3.1) - ruby2_keywords (>= 0.0.4) + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) faraday-follow_redirects (0.3.0) faraday (>= 1, < 3) - faraday-net_http (3.0.2) - ffi (1.15.5) + faraday-net_http (3.1.0) + net-http + 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) - fog-core (2.3.0) + fog-core (2.4.0) builder excon (~> 0.71) formatador (>= 0.2, < 2.0) @@ -551,13 +581,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) @@ -567,11 +597,12 @@ GEM graphql (~> 1.12) html-pipeline (~> 2.9) sass (~> 3.4) - hashdiff (1.0.1) + hashdiff (1.1.0) hashie (5.0.0) health_check (3.1.0) railties (>= 5.0) - highline (2.1.0) + highline (3.0.0) + abbrev hkdf (0.3.0) html-pipeline (2.14.3) activesupport (>= 2) @@ -592,7 +623,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.1) ice_cube (~> 0.16) ice_cube (0.16.4) ice_nine (0.11.2) @@ -602,10 +633,11 @@ GEM invisible_captcha (0.13.0) rails (>= 3.2.0) jmespath (1.6.2) - json (2.6.3) - json-jwt (1.16.3) + json (2.7.1) + json-jwt (1.16.5) activesupport (>= 4.2) aes_key_wrap + base64 bindata faraday (~> 2.0) faraday-follow_redirects @@ -637,7 +669,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,19 +684,19 @@ 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) mixlib-config (>= 2.2.1, < 4) mixlib-shellout method_source (1.0.0) - mime-types (3.5.1) + mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2023.0808) + mime-types-data (3.2023.1205) mini_magick (4.12.0) mini_mime (1.1.5) - minitest (5.19.0) + minitest (5.21.2) mixlib-cli (2.1.8) mixlib-config (3.0.27) tomlrb @@ -674,22 +706,20 @@ GEM multi_json (1.15.0) multi_xml (0.6.0) mustache (1.1.1) - net-imap (0.3.7) + net-http (0.4.1) + uri + net-imap (0.4.9.1) 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.1) net-protocol - nio4r (2.5.9) - nokogiri (1.13.4-aarch64-linux) - racc (~> 1.4) + nio4r (2.7.0) 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) @@ -705,7 +735,7 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0) version_gem (~> 1.1) - omniauth (2.1.1) + omniauth (2.1.2) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection @@ -749,10 +779,10 @@ GEM paper_trail (12.3.0) activerecord (>= 5.2) request_store (~> 1.1) - parallel (1.23.0) + parallel (1.24.0) parallel_tests (3.13.0) parallel - parser (3.2.2.3) + parser (3.3.0.4) ast (~> 2.4.1) racc pg (1.1.4) @@ -760,7 +790,7 @@ GEM activerecord (>= 5.2) activesupport (>= 5.2) polyglot (0.3.5) - premailer (1.21.0) + premailer (1.22.0) addressable css_parser (>= 1.12.0) htmlentities (>= 4.0.0) @@ -768,11 +798,11 @@ GEM actionmailer (>= 3) net-smtp premailer (~> 1.7, >= 1.7.9) - public_suffix (5.0.3) - puma (5.6.7) + public_suffix (5.0.4) + puma (5.6.8) 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) @@ -784,9 +814,10 @@ GEM httpclient json-jwt (>= 1.11.0) rack (>= 2.1.0) - rack-protection (3.1.0) + rack-protection (3.2.0) + base64 (>= 0.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 +856,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 +866,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.19.1) + connection_pool + redlock (2.0.6) + redis-client (>= 0.14.1, < 1.0.0) + regexp_parser (2.9.0) 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) @@ -851,9 +884,9 @@ GEM rspec-core (~> 3.12.0) rspec-expectations (~> 3.12.0) rspec-mocks (~> 3.12.0) - rspec-cells (0.3.8) + rspec-cells (0.3.9) cells (>= 4.0.0, < 6.0.0) - rspec-rails (>= 3.0.0, < 6.1.0) + rspec-rails (>= 3.0.0, < 6.2.0) rspec-core (3.12.2) rspec-support (~> 3.12.0) rspec-expectations (3.12.3) @@ -887,7 +920,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,9 +932,8 @@ 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) nokogiri (>= 1.10.8) rubyzip (>= 1.3.0) @@ -921,18 +953,18 @@ GEM rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2) semantic_range (3.0.0) - sendgrid-ruby (6.6.2) + sendgrid-ruby (6.7.0) ruby_http_client (~> 3.4) - sentry-rails (5.10.0) + sentry-rails (5.16.1) railties (>= 5.0) - sentry-ruby (~> 5.10.0) - sentry-ruby (5.10.0) + sentry-ruby (~> 5.16.1) + sentry-ruby (5.16.1) concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.10.0) - sentry-ruby (~> 5.10.0) + sentry-sidekiq (5.16.1) + sentry-ruby (~> 5.16.1) 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) @@ -962,33 +994,34 @@ 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) + uri (0.13.0) valid_email2 (2.3.1) activemodel (>= 3.2) mail (~> 2.5) @@ -1038,26 +1071,22 @@ 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 activejob-uniqueness + activerecord-session_store aws-sdk-s3 bootsnap (~> 1.4) brakeman (~> 5.1) @@ -1072,11 +1101,13 @@ DEPENDENCIES decidim-extended_socio_demographic_authorization_handler! decidim-extra_user_fields! decidim-friendly_signup! + decidim-gallery! decidim-homepage_interactive_map! decidim-initiatives (~> 0.27.0) decidim-ludens! decidim-phone_authorization_handler! decidim-spam_detection + decidim-survey_multiple_answers! decidim-templates (~> 0.27.0) decidim-term_customizer! deepl-rb @@ -1112,4 +1143,4 @@ RUBY VERSION ruby 3.0.6p216 BUNDLED WITH - 2.2.33 + 2.4.9 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/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/app/jobs/check_published_initiatives.rb b/app/jobs/check_published_initiatives.rb new file mode 100644 index 0000000000..14e46fd850 --- /dev/null +++ b/app/jobs/check_published_initiatives.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class CheckPublishedInitiatives < ApplicationJob + def perform + system "rake decidim_initiatives:check_published" + end +end diff --git a/app/jobs/check_validating_initiatives.rb b/app/jobs/check_validating_initiatives.rb new file mode 100644 index 0000000000..eac6e40535 --- /dev/null +++ b/app/jobs/check_validating_initiatives.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class CheckValidatingInitiatives < ApplicationJob + def perform + system "rake decidim_initiatives:check_validating" + end +end 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/app/jobs/notify_progress_initiatives.rb b/app/jobs/notify_progress_initiatives.rb new file mode 100644 index 0000000000..d8ebc0a3c0 --- /dev/null +++ b/app/jobs/notify_progress_initiatives.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class NotifyProgressInitiatives < ApplicationJob + def perform + system "rake decidim_initiatives:notify_progress" + end +end diff --git a/app/views/decidim/devise/shared/_omniauth_buttons.html.erb b/app/views/decidim/devise/shared/_omniauth_buttons.html.erb index 30b1f73def..56608e8b08 100644 --- a/app/views/decidim/devise/shared/_omniauth_buttons.html.erb +++ b/app/views/decidim/devise/shared/_omniauth_buttons.html.erb @@ -5,7 +5,7 @@
<% if provider.match?("france") %> - <%= t("devise.shared.links.sign_in_with_france_connect")%> + <%= t("devise.shared.links.sign_in_with_france_connect") %>

<%= t("decidim.omniauth.france_connect.explanation") %>

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/application.rb b/config/application.rb index 0872b7afb3..0ba9a737a4 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" @@ -40,17 +39,28 @@ class Application < Rails::Application # the framework and any gems in your application. config.to_prepare do - ActiveStorage::Blob.include ActiveStorage::Downloadable + 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" 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 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/config/environments/development.rb b/config/environments/development.rb index 93690b386b..d667dc6158 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -59,4 +59,5 @@ # are known to cause issue with moderation due to expiration # Setting this to 100 years should be enough config.global_id.expires_in = 100.years + config.deface.enabled = ENV.fetch("DEFACE_ENABLED", nil) == "true" end 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/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/config/locales/en.yml b/config/locales/en.yml index 25bdb8f15d..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 @@ -48,6 +50,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. @@ -61,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 92b7b6c56e..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 @@ -50,6 +52,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é. @@ -63,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/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/config/sidekiq.yml b/config/sidekiq.yml index e45a4a4ada..7e0045fea9 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -15,6 +15,7 @@ - reminders - active_storage_analysis - active_storage_purge + - initiatives :scheduler: :schedule: @@ -44,3 +45,15 @@ class: NotificationsDigestMailJob queue: mailers args: :weekly + CheckPublishedInitiatives: + cron: '0 1 * * *' + class: CheckPublishedInitiatives + queue: initiatives + CheckValidatingInitiatives: + cron: '0 1 * * *' + class: CheckValidatingInitiatives + queue: initiatives + NotifyProgressInitiatives: + cron: '0 1 * * *' + class: NotifyProgressInitiatives + queue: initiatives 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: 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/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 386961c075..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_08_31_093832) 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" @@ -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" @@ -2035,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.dev.yml b/docker-compose.local.yml similarity index 54% rename from docker-compose.dev.yml rename to docker-compose.local.yml index 6f6376a03f..30beb45e4e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.local.yml @@ -19,35 +19,61 @@ 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 + - RAILS_SESSION_STORE=active_record 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 + - RAILS_SESSION_STORE=active_record + volumes: + - shared-volume:/app ports: - 3000:3000 depends_on: @@ -56,6 +82,6 @@ services: - memcached volumes: - node_modules: { } + shared-volume: { } pg-data: { } - redis-data: { } + redis-data: { } \ No newline at end of file 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/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 + 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/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/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/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/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/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/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 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/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/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/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/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/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 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 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