diff --git a/.github/workflows/build_app.yml b/.github/workflows/build_app.yml new file mode 100644 index 0000000..35e54a5 --- /dev/null +++ b/.github/workflows/build_app.yml @@ -0,0 +1,66 @@ +on: + workflow_call: + inputs: + ruby_version: + description: 'Ruby Version' + default: "3.1.1" + type: string + required: false + node_version: + description: 'Node version' + default: '18.17.1' + required: false + type: string +jobs: + build_app: + name: Build app + runs-on: ubuntu-22.04 + if: "!startsWith(github.head_ref, 'chore/l10n')" + timeout-minutes: 60 + env: + DATABASE_USERNAME: postgres + DATABASE_PASSWORD: postgres + DATABASE_HOST: localhost + RUBYOPT: '-W:no-deprecated' + services: + postgres: + image: postgres:14 + ports: ["5432:5432"] + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + POSTGRES_PASSWORD: postgres + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ inputs.ruby_version }} + bundler-cache: true + - uses: actions/setup-node@v3 + with: + node-version: ${{ inputs.node_version }} + cache: 'npm' + cache-dependency-path: ./package-lock.json + - uses: actions/cache@v3 + id: app-cache + with: + path: ./spec/decidim_dummy_app/ + key: app-${{ github.sha }} + restore-keys: app-${{ github.sha }} + - run: bundle exec rake test_app + name: Create test app + shell: "bash" + - run: mkdir -p ./spec/decidim_dummy_app/tmp/screenshots + name: Create the screenshots folder + shell: "bash" + - run: RAILS_ENV=test bundle exec rails assets:precompile + name: Precompile assets + working-directory: ./spec/decidim_dummy_app/ + shell: "bash" + env: + NODE_ENV: "test" diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 5a16bc4..01affb1 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -1,16 +1,38 @@ name: "[CI] ExtraUserFields" -on: "push" +on: + push: + branches: + - develop + - release/* + - "*-stable" + pull_request: + branches-ignore: + - "chore/l10n*" + paths: + - "*" + - ".github/**" env: CI: "true" RUBY_VERSION: 3.1.1 NODE_VERSION: 18.17.1 +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: + build_app: + uses: ./.github/workflows/build_app.yml + secrets: inherit + name: Build test application + lint: + name: Lint code runs-on: ubuntu-latest + timeout-minutes: 60 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 1 - uses: actions/setup-node@master @@ -24,123 +46,11 @@ jobs: name: Lint Ruby files - run: bundle exec erblint app/**/*.erb name: Lint ERB files + tests: name: Tests - runs-on: ubuntu-latest - timeout-minutes: 30 - services: - postgres: - image: postgres:11 - ports: ["5432:5432"] - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - env: - POSTGRES_PASSWORD: postgres - env: - DATABASE_USERNAME: postgres - DATABASE_PASSWORD: postgres - DATABASE_HOST: localhost - steps: - - uses: rokroskar/workflow-run-cleanup-action@v0.3.0 - if: "github.ref != 'refs/heads/develop'" - env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - - uses: actions/checkout@v2.0.0 - with: - fetch-depth: 1 - - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - uses: actions/setup-node@v1 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Get npm cache directory path - id: npm-cache-dir-path - run: echo "::set-output name=dir::$(npm get cache)-extra_user_fields" - - uses: actions/cache@v2 - id: npm-cache - with: - path: ${{ steps.npm-cache-dir-path.outputs.dir }} - key: npm-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - npm- - - run: bundle exec rake test_app - name: Create test app - - run: mkdir -p ./spec/decidim_dummy_app/tmp/screenshots - name: Create the screenshots folder - - uses: nanasess/setup-chromedriver@v2 - - run: RAILS_ENV=test bundle exec rails assets:precompile - name: Precompile assets - working-directory: ./spec/decidim_dummy_app/ - - run: bundle exec rspec --exclude-pattern "spec/system/**/*_spec.rb" - name: RSpec - - uses: codecov/codecov-action@v1 - - uses: actions/upload-artifact@v2 - if: always() - with: - name: screenshots - path: ./spec/decidim_dummy_app/tmp/screenshots - if-no-files-found: ignore - system-tests: - name: System tests - runs-on: ubuntu-latest - timeout-minutes: 30 - services: - postgres: - image: postgres:11 - ports: ["5432:5432"] - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - env: - POSTGRES_PASSWORD: postgres - env: - DATABASE_USERNAME: postgres - DATABASE_PASSWORD: postgres - DATABASE_HOST: localhost - steps: - - uses: rokroskar/workflow-run-cleanup-action@v0.3.0 - if: "github.ref != 'refs/heads/develop'" - env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - - uses: actions/checkout@v2.0.0 - with: - fetch-depth: 1 - - uses: ruby/setup-ruby@v1 - with: - bundler-cache: true - - uses: actions/setup-node@v1 - with: - node-version: ${{ env.NODE_VERSION }} - - name: Get npm cache directory path - id: npm-cache-dir-path - run: echo "::set-output name=dir::$(npm get cache)-extra_user_fields" - - uses: actions/cache@v2 - id: npm-cache - with: - path: ${{ steps.npm-cache-dir-path.outputs.dir }} - key: npm-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - npm- - - run: bundle exec rake test_app - name: Create test app - - run: mkdir -p ./spec/decidim_dummy_app/tmp/screenshots - name: Create the screenshots folder - - uses: nanasess/setup-chromedriver@v2 - - run: RAILS_ENV=test bundle exec rails assets:precompile - name: Precompile assets - working-directory: ./spec/decidim_dummy_app/ - - run: bundle exec rspec spec/system - name: RSpec - - uses: codecov/codecov-action@v1 - - uses: actions/upload-artifact@v2 - if: always() - with: - name: screenshots - path: ./spec/decidim_dummy_app/tmp/screenshots - if-no-files-found: ignore + needs: build_app + uses: ./.github/workflows/test_app.yml + with: + test_command: "bundle exec rspec --pattern './spec/**/*_spec.rb'" + secrets: inherit diff --git a/.github/workflows/test_app.yml b/.github/workflows/test_app.yml new file mode 100644 index 0000000..f75fb10 --- /dev/null +++ b/.github/workflows/test_app.yml @@ -0,0 +1,97 @@ +on: + workflow_call: + inputs: + ruby_version: + description: 'Ruby Version' + default: "3.1.1" + required: false + type: string + test_command: + description: 'The testing command to be ran' + required: true + type: string + chrome_version: + description: 'Chrome & Chromedriver version' + required: false + default: "126.0.6478.182" + type: string + +jobs: + build_app: + name: Test app + runs-on: ubuntu-22.04 + if: "!startsWith(github.head_ref, 'chore/l10n')" + timeout-minutes: 60 + env: + DATABASE_USERNAME: postgres + DATABASE_PASSWORD: postgres + DATABASE_HOST: localhost + RUBYOPT: '-W:no-deprecated' + services: + validator: + image: ghcr.io/validator/validator:latest + ports: ["8888:8888"] + postgres: + image: postgres:14 + ports: ["5432:5432"] + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + POSTGRES_PASSWORD: postgres + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ inputs.ruby_version }} + bundler-cache: true + - run: | + sudo apt update + sudo apt install libu2f-udev + wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${{inputs.chrome_version}}-1_amd64.deb + sudo dpkg -i /tmp/chrome.deb + rm /tmp/chrome.deb + - uses: nanasess/setup-chromedriver@v2 + name: Install Chrome version ${{inputs.chrome_version}} + with: + chromedriver-version: ${{inputs.chrome_version}} + - uses: actions/cache@v3 + id: app-cache + with: + path: ./spec/decidim_dummy_app/ + key: app-${{ github.sha }} + restore-keys: app-${{ github.sha }} + - run: bundle exec rails db:create db:schema:load + name: Install gems and create db + shell: "bash" + working-directory: ./spec/decidim_dummy_app/ + - run: | + sudo Xvfb -ac $DISPLAY -screen 0 1920x1084x24 > /dev/null 2>&1 & + ${{ inputs.test_command }} + name: RSpec + working-directory: ./ + env: + VALIDATOR_HTML_URI: http://localhost:8888/ + RUBY_VERSION: ${{ inputs.ruby_version }} + DECIDIM_MODULE: ${{ inputs.working-directory }} + DISPLAY: ":99" + CI: "true" + SIMPLECOV: "true" + SHAKAPACKER_RUNTIME_COMPILE: "false" + NODE_ENV: "test" + - uses: codecov/codecov-action@v3 + name: Upload coverage + with: + name: ${{ inputs.working-directory }} + flags: ${{ inputs.working-directory }} + - uses: actions/upload-artifact@v3 + if: always() + with: + name: screenshots + path: ./spec/decidim_dummy_app/tmp/screenshots + if-no-files-found: ignore + overwrite: true diff --git a/Gemfile b/Gemfile index c73393d..3c5b45d 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,6 @@ gem "puma", ">= 4.3" group :development, :test do gem "byebug", "~> 11.0", platform: :mri - gem "decidim-dev", DECIDIM_VERSION end @@ -28,3 +27,8 @@ group :development do gem "spring-watcher-listen", "~> 2.0" gem "web-console", "~> 3.5" end + +group :test do + gem "rubocop-factory_bot", "!= 2.26.0", require: false + gem "rubocop-rspec_rails", "!= 2.29.0", require: false +end diff --git a/Gemfile.lock b/Gemfile.lock index ae42df3..50b339b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -490,6 +490,8 @@ GEM net-smtp (0.3.4) net-protocol nio4r (2.7.3) + nokogiri (1.16.6-x86_64-darwin) + racc (~> 1.4) nokogiri (1.16.6-x86_64-linux) racc (~> 1.4) oauth (1.1.0) @@ -669,7 +671,7 @@ GEM parser (>= 3.3.1.0) rubocop-capybara (2.21.0) rubocop (~> 1.41) - rubocop-factory_bot (2.26.0) + rubocop-factory_bot (2.25.1) rubocop (~> 1.41) rubocop-faker (1.1.0) faker (>= 2.12.0) @@ -684,7 +686,7 @@ GEM rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) rubocop-rspec_rails (~> 2.28) - rubocop-rspec_rails (2.29.0) + rubocop-rspec_rails (2.28.3) rubocop (~> 1.40) ruby-progressbar (1.13.0) ruby-vips (2.2.1) @@ -794,6 +796,7 @@ GEM zeitwerk (2.6.16) PLATFORMS + x86_64-darwin-24 x86_64-linux DEPENDENCIES @@ -807,7 +810,9 @@ DEPENDENCIES letter_opener_web (~> 1.3) listen (~> 3.1) puma (>= 4.3) + rubocop-factory_bot (!= 2.26.0) rubocop-faker + rubocop-rspec_rails (!= 2.29.0) spring (~> 2.0) spring-watcher-listen (~> 2.0) web-console (~> 3.5) diff --git a/app/commands/concerns/decidim/extra_user_fields/create_registrations_commands_overrides.rb b/app/commands/concerns/decidim/extra_user_fields/create_registrations_commands_overrides.rb index 6a9d4d5..dd93ac9 100644 --- a/app/commands/concerns/decidim/extra_user_fields/create_registrations_commands_overrides.rb +++ b/app/commands/concerns/decidim/extra_user_fields/create_registrations_commands_overrides.rb @@ -8,6 +8,24 @@ module ExtraUserFields module CreateRegistrationsCommandsOverrides extend ActiveSupport::Concern + def call + return broadcast(:invalid) if same_email_representative? + + if form.invalid? + + user = User.has_pending_invitations?(form.current_organization.id, form.email) + user.invite!(user.invited_by) if user + return broadcast(:invalid) + end + + create_user + send_email_to_statutory_representative + + broadcast(:ok, @user) + rescue ActiveRecord::RecordInvalid + broadcast(:invalid) + end + private def create_user @@ -33,9 +51,23 @@ def extended_data date_of_birth: form.date_of_birth, gender: form.gender, phone_number: form.phone_number, - location: form.location + location: form.location, + underage: form.underage, + statutory_representative_email: form.statutory_representative_email ) end + + def send_email_to_statutory_representative + return if form.statutory_representative_email.blank? || form.underage != "1" + + Decidim::ExtraUserFields::StatutoryRepresentativeMailer.inform(@user).deliver_later + end + + def same_email_representative? + return false if form.statutory_representative_email.blank? + + form.statutory_representative_email == form.email + end end end end diff --git a/app/commands/concerns/decidim/extra_user_fields/omniauth_commands_overrides.rb b/app/commands/concerns/decidim/extra_user_fields/omniauth_commands_overrides.rb index a56d481..1cfe973 100644 --- a/app/commands/concerns/decidim/extra_user_fields/omniauth_commands_overrides.rb +++ b/app/commands/concerns/decidim/extra_user_fields/omniauth_commands_overrides.rb @@ -8,6 +8,33 @@ module ExtraUserFields module OmniauthCommandsOverrides extend ActiveSupport::Concern + def call + return broadcast(:invalid) if same_email_representative? + + verify_oauth_signature! + + begin + if existing_identity + user = existing_identity.user + verify_user_confirmed(user) + + return broadcast(:ok, user) + end + return broadcast(:invalid) if form.invalid? + + transaction do + create_or_find_user + send_email_to_statutory_representative + @identity = create_identity + end + trigger_omniauth_registration + + broadcast(:ok, @user) + rescue ActiveRecord::RecordInvalid => e + broadcast(:error, e.record) + end + end + private def create_or_find_user @@ -51,9 +78,23 @@ def extended_data date_of_birth: form.date_of_birth, gender: form.gender, phone_number: form.phone_number, - location: form.location + location: form.location, + underage: form.underage, + statutory_representative_email: form.statutory_representative_email ) end + + def send_email_to_statutory_representative + return if form.statutory_representative_email.blank? || form.underage != "1" + + Decidim::ExtraUserFields::StatutoryRepresentativeMailer.inform(@user).deliver_later + end + + def same_email_representative? + return false if form.statutory_representative_email.blank? + + form.statutory_representative_email == form.email + end end end end diff --git a/app/commands/concerns/decidim/extra_user_fields/update_account_commands_overrides.rb b/app/commands/concerns/decidim/extra_user_fields/update_account_commands_overrides.rb index 2f0f081..2907c1e 100644 --- a/app/commands/concerns/decidim/extra_user_fields/update_account_commands_overrides.rb +++ b/app/commands/concerns/decidim/extra_user_fields/update_account_commands_overrides.rb @@ -11,6 +11,7 @@ module UpdateAccountCommandsOverrides private def update_personal_data + @user.locale = @form.locale @user.name = @form.name @user.nickname = @form.nickname @user.email = @form.email @@ -26,7 +27,9 @@ def extended_data date_of_birth: @form.date_of_birth, gender: @form.gender, phone_number: @form.phone_number, - location: @form.location + location: @form.location, + underage: @form.underage, + statutory_representative_email: @form.statutory_representative_email ) end end diff --git a/app/commands/decidim/extra_user_fields/admin/update_extra_user_fields.rb b/app/commands/decidim/extra_user_fields/admin/update_extra_user_fields.rb index f3d01c2..d2e7443 100644 --- a/app/commands/decidim/extra_user_fields/admin/update_extra_user_fields.rb +++ b/app/commands/decidim/extra_user_fields/admin/update_extra_user_fields.rb @@ -38,7 +38,7 @@ def update_extra_user_fields! ) end - # rubocop:disable Style/TrailingCommaInHashLiteral + # rubocop:disable Metrics/CyclomaticComplexity def extra_user_fields { "enabled" => form.enabled.presence || false, @@ -52,12 +52,14 @@ def extra_user_fields "placeholder" => form.phone_number_placeholder.presence }, "location" => { "enabled" => form.location.presence || false }, + "underage" => { "enabled" => form.underage || false }, + "underage_limit" => form.underage_limit || Decidim::ExtraUserFields::Engine::DEFAULT_UNDERAGE_LIMIT # Block ExtraUserFields SaveFieldInConfig # EndBlock } end - # rubocop:enable Style/TrailingCommaInHashLiteral + # rubocop:enable Metrics/CyclomaticComplexity end end end diff --git a/app/controllers/decidim/extra_user_fields/admin/extra_user_fields_controller.rb b/app/controllers/decidim/extra_user_fields/admin/extra_user_fields_controller.rb index fbec1ca..2216a53 100644 --- a/app/controllers/decidim/extra_user_fields/admin/extra_user_fields_controller.rb +++ b/app/controllers/decidim/extra_user_fields/admin/extra_user_fields_controller.rb @@ -38,11 +38,18 @@ def update def export_users enforce_permission_to :read, :officialization - ExportUsers.call(params[:format], current_user) do - on(:ok) do |export_data| - send_data export_data.read, type: "text/#{export_data.extension}", filename: export_data.filename("participants") - end + Decidim.traceability.perform_action!("export_users", current_organization, current_user, { format: params[:format] }) do + ExportParticipantsJob.perform_later(current_organization, current_user, params[:format]) end + + flash[:notice] = t("decidim.admin.exports.notice") + redirect_to engine_routes.officializations_path + end + + private + + def engine_routes + Decidim::Admin::Engine.routes.url_helpers end end end diff --git a/app/controllers/decidim/extra_user_fields/extra_user_fields_controller.rb b/app/controllers/decidim/extra_user_fields/extra_user_fields_controller.rb new file mode 100644 index 0000000..93415ef --- /dev/null +++ b/app/controllers/decidim/extra_user_fields/extra_user_fields_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Decidim + module ExtraUserFields + # This controller is the abstract class from which all other controllers of + # this engine inherit. + class ExtraUserFieldsController < ApplicationController + def retrieve_underage_limit + underage_limit = current_organization.extra_user_fields["underage_limit"] + if underage_limit.present? + render json: { underage_limit: } + else + render json: { error: "Underage limit not found" }, status: :not_found + end + end + end + end +end diff --git a/app/forms/concerns/decidim/extra_user_fields/forms_definitions.rb b/app/forms/concerns/decidim/extra_user_fields/forms_definitions.rb index 0466545..982d7ac 100644 --- a/app/forms/concerns/decidim/extra_user_fields/forms_definitions.rb +++ b/app/forms/concerns/decidim/extra_user_fields/forms_definitions.rb @@ -19,6 +19,8 @@ module FormsDefinitions attribute :gender, String attribute :phone_number, String attribute :location, String + attribute :underage, ActiveRecord::Type::Boolean + attribute :statutory_representative_email, String # EndBlock @@ -36,6 +38,12 @@ module FormsDefinitions ) validates :location, presence: true, if: :location? + validates :underage, presence: true, if: :underage? + validates :statutory_representative_email, + presence: true, + "valid_email_2/email": { disposable: true }, + if: :underage_accepted? + validate :birth_date_under_limit # EndBlock end @@ -49,6 +57,8 @@ def map_model(model) self.gender = extended_data[:gender] self.phone_number = extended_data[:phone_number] self.location = extended_data[:location] + self.underage = extended_data[:underage] + self.statutory_representative_email = extended_data[:statutory_representative_email] # Block ExtraUserFields MapModel @@ -88,11 +98,47 @@ def location? extra_user_fields_enabled && current_organization.activated_extra_field?(:location) end + def underage? + extra_user_fields_enabled && current_organization.activated_extra_field?(:underage) + end + + def underage_accepted? + underage? && underage == "1" + end + # EndBlock def extra_user_fields_enabled @extra_user_fields_enabled ||= current_organization.extra_user_fields_enabled? end + + # Method to check if birth date is under the limit + def birth_date_under_limit + return unless date_of_birth? && underage? + + return if date_of_birth.blank? || underage.blank? || underage_limit.blank? + + age = calculate_age(date_of_birth) + + validate_age(age) + end + + def calculate_age(date_of_birth) + Time.zone.today.year - date_of_birth.year - (Time.zone.today.yday < date_of_birth.yday ? 1 : 0) + end + + def validate_age(age) + errors.add(:date_of_birth, :underage) unless underage_within_limit?(age) + underage_within_limit?(age) + end + + def underage_within_limit?(age) + (date_of_birth.present? && age < underage_limit && underage_accepted?) || (age > underage_limit && !underage_accepted?) + end + + def underage_limit + current_organization.extra_user_fields["underage_limit"] + end end end end diff --git a/app/forms/decidim/extra_user_fields/admin/extra_user_fields_form.rb b/app/forms/decidim/extra_user_fields/admin/extra_user_fields_form.rb index 185d6f0..5354b5c 100644 --- a/app/forms/decidim/extra_user_fields/admin/extra_user_fields_form.rb +++ b/app/forms/decidim/extra_user_fields/admin/extra_user_fields_form.rb @@ -13,6 +13,8 @@ class ExtraUserFieldsForm < Decidim::Form attribute :gender, Boolean attribute :phone_number, Boolean attribute :location, Boolean + attribute :underage, Boolean + attribute :underage_limit, Integer attribute :phone_number_pattern, String translatable_attribute :phone_number_placeholder, String @@ -28,6 +30,8 @@ def map_model(model) self.gender = model.extra_user_fields.dig("gender", "enabled") self.phone_number = model.extra_user_fields.dig("phone_number", "enabled") self.location = model.extra_user_fields.dig("location", "enabled") + self.underage = model.extra_user_fields.dig("underage", "enabled") + self.underage_limit = model.extra_user_fields.fetch("underage_limit", Decidim::ExtraUserFields::Engine::DEFAULT_UNDERAGE_LIMIT) self.phone_number_pattern = model.extra_user_fields.dig("phone_number", "pattern") self.phone_number_placeholder = model.extra_user_fields.dig("phone_number", "placeholder") # Block ExtraUserFields MapModel diff --git a/app/jobs/decidim/extra_user_fields/admin/export_participants_job.rb b/app/jobs/decidim/extra_user_fields/admin/export_participants_job.rb new file mode 100644 index 0000000..337c126 --- /dev/null +++ b/app/jobs/decidim/extra_user_fields/admin/export_participants_job.rb @@ -0,0 +1,17 @@ +# frozen_string_literal = true + +module Decidim + module ExtraUserFields + module Admin + class ExportParticipantsJob < ApplicationJob + queue_as :exports + + def perform(organization, user, format) + collection = organization.users.not_deleted + export_data = Decidim::Exporters.find_exporter(format).new(collection, Decidim::ExtraUserFields::UserExportSerializer).export + ExportMailer.export(user, "participants", export_data).deliver_now + end + end + end + end +end diff --git a/app/mailers/decidim/extra_user_fields/statutory_representative_mailer.rb b/app/mailers/decidim/extra_user_fields/statutory_representative_mailer.rb new file mode 100644 index 0000000..b11df3f --- /dev/null +++ b/app/mailers/decidim/extra_user_fields/statutory_representative_mailer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Decidim + module ExtraUserFields + class StatutoryRepresentativeMailer < ApplicationMailer + def inform(user) + return if user.email.blank? + return if user.extended_data["statutory_representative_email"].blank? + + @user = user + @statutory_representative_email = user.extended_data["statutory_representative_email"] + @organization = user.organization + + with_user(user) do + @subject = I18n.t("inform.subject", scope: "decidim.statutory_representative") + @body = I18n.t("inform.body", scope: "decidim.statutory_representative", name: user.name, nickname: user.nickname, organization: @organization.name) + + mail(from: Decidim.config.mailer_sender, to: "#{@statutory_representative_email} <#{@statutory_representative_email}>", subject: @subject) + end + end + end + end +end diff --git a/app/models/concerns/decidim/extra_user_fields/organization_overrides.rb b/app/models/concerns/decidim/extra_user_fields/organization_overrides.rb index 7784ca6..241c2ed 100644 --- a/app/models/concerns/decidim/extra_user_fields/organization_overrides.rb +++ b/app/models/concerns/decidim/extra_user_fields/organization_overrides.rb @@ -14,7 +14,7 @@ def extra_user_fields_enabled? end def at_least_one_extra_field? - extra_user_fields.reject { |key| key == "enabled" } + extra_user_fields.reject { |key| %w(enabled underage_limit).include?(key) } .map { |_, value| value["enabled"] }.any? end @@ -23,6 +23,10 @@ def activated_extra_field?(sym) extra_user_fields.dig(sym.to_s, "enabled") == true end + def age_limit? + extra_user_fields["underage_limit"].to_i + end + def extra_user_field_configuration(sym) return {} unless activated_extra_field?(sym) diff --git a/app/overrides/decidim/admin/officializations/index/_export_users_dropdown.html.erb.deface b/app/overrides/decidim/admin/officializations/index/_export_users_dropdown.html.erb.deface index fbc8047..38c25ae 100644 --- a/app/overrides/decidim/admin/officializations/index/_export_users_dropdown.html.erb.deface +++ b/app/overrides/decidim/admin/officializations/index/_export_users_dropdown.html.erb.deface @@ -1,3 +1,3 @@ - + <%= render partial: "decidim/extra_user_fields/admin/export_users/dropdown" %> diff --git a/app/packs/entrypoints/decidim_extra_user_fields.js b/app/packs/entrypoints/decidim_extra_user_fields.js new file mode 100644 index 0000000..1264f1e --- /dev/null +++ b/app/packs/entrypoints/decidim_extra_user_fields.js @@ -0,0 +1,4 @@ +// Images +require.context("../images", true) + +import "src/decidim/extra_user_fields/signup_form" diff --git a/app/packs/entrypoints/decidim_extra_user_fields.scss b/app/packs/entrypoints/decidim_extra_user_fields.scss new file mode 100644 index 0000000..6b0fef8 --- /dev/null +++ b/app/packs/entrypoints/decidim_extra_user_fields.scss @@ -0,0 +1 @@ +@import "stylesheets/decidim/extra_user_fields/signup_form"; diff --git a/app/packs/images/decidim/extra_user_fields/entrypoints/extra_user_fields.js b/app/packs/images/decidim/extra_user_fields/entrypoints/extra_user_fields.js deleted file mode 100644 index a516e90..0000000 --- a/app/packs/images/decidim/extra_user_fields/entrypoints/extra_user_fields.js +++ /dev/null @@ -1,2 +0,0 @@ -// Images -require.context("../images", true) diff --git a/app/packs/src/decidim/extra_user_fields/signup_form.js b/app/packs/src/decidim/extra_user_fields/signup_form.js new file mode 100644 index 0000000..12739c7 --- /dev/null +++ b/app/packs/src/decidim/extra_user_fields/signup_form.js @@ -0,0 +1,66 @@ +$(document).ready(function() { + const underageCheckbox = $('#registration_underage_checkbox'); + const statutoryRepresentativeEmailField = $('#statutory_representative_email_field'); + const dateOfBirthField = $('#registration_user_date_of_birth'); + const underageFieldSet = $('#underage_fieldset'); + let underageLimit = 18; + + // Function to show or hide underage related fields based on age + function updateUnderageFields() { + const dobValue = dateOfBirthField.val(); + //const dobParts = dobValue.split('-'); + //const dobDate = Date.parse(`${dobParts[1]}-${dobParts[0]}-${dobParts[2]}`); + const dobDate = Date.parse(`${dobValue}`) + const currentDate = Date.now(); + const ageInMilliseconds = currentDate - dobDate; + const age = Math.abs(new Date(ageInMilliseconds).getUTCFullYear() - 1970); + + if (age < underageLimit) { + underageFieldSet.removeClass('hidden'); + underageCheckbox.prop('checked', true); + statutoryRepresentativeEmailField.removeClass('hidden'); + } else { + statutoryRepresentativeEmailField.find('input').val(''); + underageFieldSet.addClass('hidden'); + underageCheckbox.prop('checked', false); + statutoryRepresentativeEmailField.addClass('hidden'); + } + } + + if (underageCheckbox.length && statutoryRepresentativeEmailField.length) { + underageCheckbox.on('change', function() { + if (underageCheckbox.prop('checked')) { + statutoryRepresentativeEmailField.removeClass('hidden'); + } else { + statutoryRepresentativeEmailField.find('input').val(''); + statutoryRepresentativeEmailField.addClass('hidden'); + } + }); + } + + if (dateOfBirthField.length && underageCheckbox.length) { + updateUnderageFields(); + } + + if (underageCheckbox.length) { + $.ajax({ + url: '/extra_user_fields/underage_limit', // Updated to match the new route + type: 'GET', + success: function (data) { + underageLimit = data.underage_limit; + if (dateOfBirthField.length && underageFieldSet.length) { + updateUnderageFields(); + } + }, + error: function (jqXHR, textStatus, errorThrown) { + console.error("Failed to fetch underage limit:", textStatus, errorThrown); + } + }); + } + + if (dateOfBirthField.length && underageFieldSet.length) { + dateOfBirthField.on('change', function() { + updateUnderageFields(); + }); + } +}); diff --git a/app/packs/stylesheets/decidim/extra_user_fields/signup_form.scss b/app/packs/stylesheets/decidim/extra_user_fields/signup_form.scss new file mode 100644 index 0000000..f4d336f --- /dev/null +++ b/app/packs/stylesheets/decidim/extra_user_fields/signup_form.scss @@ -0,0 +1,3 @@ +.hidden { + display: none; +} diff --git a/app/serializers/decidim/extra_user_fields/user_export_serializer.rb b/app/serializers/decidim/extra_user_fields/user_export_serializer.rb index d9b748d..6e9c444 100644 --- a/app/serializers/decidim/extra_user_fields/user_export_serializer.rb +++ b/app/serializers/decidim/extra_user_fields/user_export_serializer.rb @@ -13,12 +13,11 @@ def serialize def extra_user_fields extended_data = resource.extended_data.symbolize_keys - [:gender, :country, :postal_code, :date_of_birth, :phone_number, :location].index_with do |key| + [:gender, :country, :postal_code, :date_of_birth, :phone_number, :location, :underage, :statutory_representative_email].index_with do |key| extended_data[key] end end - # rubocop:disable Style/TrailingCommaInArrayLiteral def extra_fields [ :gender, @@ -27,12 +26,13 @@ def extra_fields :date_of_birth, :phone_number, :location, + :underage, + :statutory_representative_email # Block ExtraUserFields AddExtraField # EndBlock ] end - # rubocop:enable Style/TrailingCommaInArrayLiteral end end end diff --git a/app/views/decidim/extra_user_fields/_profile_form.html.erb b/app/views/decidim/extra_user_fields/_profile_form.html.erb index 4d82d2a..46473e0 100644 --- a/app/views/decidim/extra_user_fields/_profile_form.html.erb +++ b/app/views/decidim/extra_user_fields/_profile_form.html.erb @@ -24,4 +24,9 @@ <% if current_organization.activated_extra_field?(:location) %> <%= f.text_field :location %> <% end %> + + <% if current_organization.activated_extra_field?(:underage) %> + <%= f.hidden_field :underage, value: current_user.extended_data["underage"] || "0" %> + <%= f.hidden_field :statutory_representative_email, value: current_user.extended_data["statutory_representative_email"] || "" %> + <% end %> <% end %> diff --git a/app/views/decidim/extra_user_fields/_registration_form.html.erb b/app/views/decidim/extra_user_fields/_registration_form.html.erb index 1c1d85e..73567bb 100644 --- a/app/views/decidim/extra_user_fields/_registration_form.html.erb +++ b/app/views/decidim/extra_user_fields/_registration_form.html.erb @@ -1,9 +1,9 @@ <% if current_organization.extra_user_fields_enabled? %>
-

<%= t(".signup.legend") %>

+

<%= t(".signup.legend") %>

<% if current_organization.activated_extra_field?(:date_of_birth) %> - <%= f.date_field :date_of_birth %> + <%= f.date_field :date_of_birth, id: "user_date_of_birth" %> <% end %> <% if current_organization.activated_extra_field?(:gender) %> @@ -28,9 +28,30 @@ <%= f.text_field :location %> <% end %> + <% if current_organization.activated_extra_field?(:underage) %> + <% if current_organization.activated_extra_field?(:date_of_birth) %> + + <% else %> +
+ <%= f.check_box :underage, id: "underage_checkbox", label: t(".signup.underage", limit: current_organization.age_limit?) %> + +
+ <%end %> + <% end %> <%# Block ExtraUserFields SignUpFormFields %> <%# EndBlock %>
+ <%= append_javascript_pack_tag "decidim_extra_user_fields.js" %> + <%= append_stylesheet_pack_tag "decidim_extra_user_fields_css" %> <% end %> + + diff --git a/app/views/decidim/extra_user_fields/admin/extra_user_fields/_form.html.erb b/app/views/decidim/extra_user_fields/admin/extra_user_fields/_form.html.erb index 30da0e8..dc61bb0 100644 --- a/app/views/decidim/extra_user_fields/admin/extra_user_fields/_form.html.erb +++ b/app/views/decidim/extra_user_fields/admin/extra_user_fields/_form.html.erb @@ -31,6 +31,7 @@ <%= render partial: "decidim/extra_user_fields/admin/extra_user_fields/fields/gender", locals: { form: form } %> <%= render partial: "decidim/extra_user_fields/admin/extra_user_fields/fields/phone_number", locals: { form: form } %> <%= render partial: "decidim/extra_user_fields/admin/extra_user_fields/fields/location", locals: { form: form } %> + <%= render partial: "decidim/extra_user_fields/admin/extra_user_fields/fields/underage", locals: { form: form } %> diff --git a/app/views/decidim/extra_user_fields/admin/extra_user_fields/fields/_underage.html.erb b/app/views/decidim/extra_user_fields/admin/extra_user_fields/fields/_underage.html.erb new file mode 100644 index 0000000..2713630 --- /dev/null +++ b/app/views/decidim/extra_user_fields/admin/extra_user_fields/fields/_underage.html.erb @@ -0,0 +1,7 @@ +
+
+

<%= t(".description") %>

+ <%= form.check_box :underage, label: t(".label") %> + <%= form.select :underage_limit, ( Decidim::ExtraUserFields::Engine::DEFAULT_UNDERAGE_OPTIONS).to_a, selected: current_organization.extra_user_fields["underage_limit"] || Decidim::ExtraUserFields::Engine::DEFAULT_UNDERAGE_LIMIT, label: t(".limit") %> +
+
diff --git a/app/views/decidim/extra_user_fields/statutory_representative_mailer/inform.html.erb b/app/views/decidim/extra_user_fields/statutory_representative_mailer/inform.html.erb new file mode 100644 index 0000000..ee6b892 --- /dev/null +++ b/app/views/decidim/extra_user_fields/statutory_representative_mailer/inform.html.erb @@ -0,0 +1 @@ +

<%== @body %>

diff --git a/config/assets.rb b/config/assets.rb new file mode 100644 index 0000000..9be6ca7 --- /dev/null +++ b/config/assets.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# This file is located at `config/assets.rb` of your module. + +# Define the base path of your module. Please note that `Rails.root` may not be +# used because we are not inside the Rails environment when this file is loaded. +base_path = File.expand_path("..", __dir__) + +# Register an additional load path for webpack. All the assets within these +# directories will be available for inclusion within the Decidim assets. For +# example, if you have `app/packs/src/decidim/foo.js`, you can include that file +# in your JavaScript entrypoints (or other JavaScript files within Decidim) +# using `import "src/decidim/foo"` after you have registered the additional path +# as follows. +Decidim::Webpacker.register_path("#{base_path}/app/packs") + +# Register the entrypoints for your module. These entrypoints can be included +# within your application using `javascript_pack_tag` and if you include any +# SCSS files within the entrypoints, they become available for inclusion using +# `stylesheet_pack_tag`. +Decidim::Webpacker.register_entrypoints( + decidim_extra_user_fields: "#{base_path}/app/packs/entrypoints/decidim_extra_user_fields.js", + decidim_extra_user_fields_css: "#{base_path}/app/packs/entrypoints/decidim_extra_user_fields.scss" +) + +# If you want to import some extra SCSS files in the Decidim main SCSS file +# without adding any extra stylesheet inclusion tags, you can use the following +# method to register the stylesheet import for the main application. +# Decidim::Webpacker.register_stylesheet_import("stylesheets/decidim/homepage_interactive_map/map.scss") diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index ffb4faa..aa01e24 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -6,8 +6,10 @@ locales: [en] ignore_unused: - "decidim.components.extra_user_fields.name" - activemodel.attributes.user.* + - activemodel.errors.models.user.* - decidim.admin.extra_user_fields.menu.title - decidim.extra_user_fields.genders.* ignore_missing: - decidim.participatory_processes.scopes.global + - decidim.admin.exports.notice diff --git a/config/locales/de.yml b/config/locales/de.yml new file mode 100644 index 0000000..50d9c8d --- /dev/null +++ b/config/locales/de.yml @@ -0,0 +1,82 @@ +--- +de: + activemodel: + attributes: + user: + country: Land + date_of_birth: Geburtsdatum + gender: Geschlecht + location: Standort + phone_number: Telefonnummer + postal_code: Postleitzahl + errors: + models: + user: + attributes: + date_of_birth: + underage: ungültig. Wenn Sie minderjährig sind, müssen Sie die Erlaubnis der Eltern einholen + decidim: + admin: + actions: + export: Exportieren + exports: + export_as: Exportieren im Format %{export_format} + extra_user_fields: + menu: + title: Benutzerdefinierte Anmeldefelder verwalten + components: + extra_user_fields: + name: Benutzerdefinierte Anmeldefelder + extra_user_fields: + admin: + exports: + users: Teilnehmer + extra_user_fields: + fields: + country: + description: Dieses Feld enthält eine Liste von Ländern. Der Benutzer kann ein Land auswählen. + label: Das Feld Land aktivieren + date_of_birth: + description: Dieses Feld ist ein Feld für das Geburtsdatum. Der Benutzer kann ein Datum auswählen. + label: Das Feld Geburtsdatum aktivieren + gender: + description: Dieses Feld ist ein Feld für die Geschlechtsidentität. Der Benutzer kann ein Geschlecht auswählen. + label: Das Feld Geschlecht aktivieren + location: + description: Dieses Feld ermöglicht das Hinzufügen von Text. Der Benutzer kann einen Ort auswählen. + label: Das Feld Standort aktivieren + phone_number: + description: Dieses Feld ist ein Telefonnummernfeld. Der Benutzer kann eine Nummer auswählen. + label: Das Feld Telefonnummer aktivieren + postal_code: + description: Dieses Feld ist für die Postleitzahl. Der Benutzer kann eine Postleitzahl auswählen. + label: Das Postleitzahlenfeld aktivieren. + form: + callout: + help: Aktivieren Sie die Funktion für benutzerdefinierte Anmeldefelder, um zusätzliche Felder in Ihrem Anmeldeformular zu verwalten. Auch bei aktivierter Option wird das Anmeldeformular nur aktualisiert, wenn mindestens ein zusätzliches Feld aktiviert ist. + extra_user_fields: + extra_user_fields_enabled: Benutzerdefinierte Anmeldefelder aktivieren + section: Verfügbare Anmeldefelder für das Anmeldeformular + global: + title: Aktivieren / Deaktivieren von benutzerdefinierten Anmeldefeldern + index: + save: Speichern + title: Benutzerdefinierte Anmeldefelder verwalten + update: + failure: Bei der Aktualisierung ist ein Fehler aufgetreten. + success: Die Anmeldefelder wurden erfolgreich aktualisiert. + genders: + female: Frau + male: Mann + other: Divers + registration_form: + signup: + legend: Weitere Informationen + statutory_representative: + inform: + body: | + Hallo, + Sie wurden als gesetzlicher Vertreter von %{name} für die Registrierung bei %{organization} benannt. + Beste grüße, + Das %{organization} Team + subject: Sie wurden als gesetzlicher Vertreter benannt diff --git a/config/locales/en.yml b/config/locales/en.yml index d592c05..af86e18 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -9,6 +9,14 @@ en: location: Location phone_number: Phone Number postal_code: Postal code + statutory_representative_email: Representative email + underage: Underage + errors: + models: + user: + attributes: + date_of_birth: + underage: invalid. If you are underage, you must obtain parental authorization decidim: admin: actions: @@ -57,6 +65,11 @@ en: description: This field is a String field. If checked, user will have to fill in a postal code label: Enable postal code field + underage: + description: This field is a Boolean field. User will be able to check + if is underage + label: Enable parental authorization field + limit: This sets the age limit (ex. 18 years old) form: callout: help: Enable custom extra user fields functionality to be able to manage @@ -80,3 +93,12 @@ en: registration_form: signup: legend: More information + underage: I am under %{limit} years old and I agree to get a parental authorization + statutory_representative: + inform: + body: | + Hello, + You have been designated as the legal representative of %{name} for their registration with %{organization}. + Best regards, + The %{organization} Team + subject: You have been designated as the legal representative diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a82b997..fd9beea 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -9,6 +9,15 @@ fr: location: Localisation phone_number: Numéro de téléphone postal_code: Code postal + statutory_representative_email: Email du représentant légal + underage: Mineur + errors: + models: + user: + attributes: + date_of_birth: + underage: invalide. Si vous êtes mineur, vous devez obtenir une autorisation + parentale decidim: admin: actions: @@ -57,6 +66,11 @@ fr: description: Ce champ est un champ code postal. L'utilisateur pourra choisir un code postal. label: Activer le champ code postal + underage: + description: Ce champ est un champ booléen. L'utilisateur pourra cocher + s'il est mineur. + label: Activer le champ d'autorisation parentale + limit: Cela définit la limite d'âge (ex. 18 ans) form: callout: help: Activez la fonctionnalité des champs d'inscription personnalisés @@ -81,3 +95,13 @@ fr: registration_form: signup: legend: Plus d'information + underage: Je suis âgé de moins de %{limit} ans et j'accepte d'obtenir une + autorisation parentale + statutory_representative: + inform: + body: | + Bonjour, + Vous avez été désigné comme représentant légal de %{name} pour son inscription à %{organization}. + Cordialement, + L'équipe de %{organization} + subject: Vous avez été désigné comme représentant légal diff --git a/lib/decidim/extra_user_fields/engine.rb b/lib/decidim/extra_user_fields/engine.rb index 655fb04..e6a1cb6 100644 --- a/lib/decidim/extra_user_fields/engine.rb +++ b/lib/decidim/extra_user_fields/engine.rb @@ -13,10 +13,15 @@ class Engine < ::Rails::Engine DEFAULT_GENDER_OPTIONS = [:male, :female, :other].freeze + DEFAULT_UNDERAGE_LIMIT = 18 + + DEFAULT_UNDERAGE_OPTIONS = (15..21) + routes do # Add engine routes here # resources :extra_user_fields # root to: "extra_user_fields#index" + get "underage_limit", to: "extra_user_fields#retrieve_underage_limit", as: :retrieve_underage_limit end initializer "decidim_extra_user_fields.registration_additions" do @@ -54,6 +59,12 @@ class Engine < ::Rails::Engine end end end + + initializer "decidim_extra_user_fields.mount_routes" do + Decidim::Core::Engine.routes do + mount Decidim::ExtraUserFields::Engine, at: "/extra_user_fields", as: "decidim_extra_user_fields_engine" + end + end end end end diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e83bb48 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "decidim-module-extra_user_fields", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/spec/commands/decidim/create_omniauth_registration_spec.rb b/spec/commands/decidim/create_omniauth_registration_spec.rb index 3a44850..a87436a 100644 --- a/spec/commands/decidim/create_omniauth_registration_spec.rb +++ b/spec/commands/decidim/create_omniauth_registration_spec.rb @@ -18,6 +18,8 @@ module Comments let(:location) { "Paris" } let(:phone_number) { "0123456789" } let(:postal_code) { "75001" } + let(:underage) { false } + let(:statutory_representative_email) { nil } let(:extended_data) do { country:, @@ -25,7 +27,9 @@ module Comments gender:, location:, phone_number:, - postal_code: + postal_code:, + underage:, + statutory_representative_email: } end @@ -45,7 +49,9 @@ module Comments "date_of_birth" => date_of_birth, "gender" => gender, "phone_number" => phone_number, - "location" => location + "location" => location, + "underage" => underage, + "statutory_representative_email" => statutory_representative_email } } end @@ -147,7 +153,6 @@ module Comments it "links a previously existing user" do user = create(:user, email:, organization:) expect { command.call }.not_to change(User, :count) - expect(user.identities.length).to eq(1) end @@ -227,6 +232,15 @@ module Comments expect(user).not_to be_confirmed end end + + context "when the user is underage and tries to duplicate email" do + let(:underage) { true } + let(:statutory_representative_email) { email } + + it "broadcasts invalid" do + expect { command.call }.to broadcast(:invalid) + end + end end end end diff --git a/spec/commands/decidim/create_registration_spec.rb b/spec/commands/decidim/create_registration_spec.rb index 6760b25..080fde6 100644 --- a/spec/commands/decidim/create_registration_spec.rb +++ b/spec/commands/decidim/create_registration_spec.rb @@ -21,6 +21,8 @@ module Comments let(:location) { "Paris" } let(:phone_number) { "0123456789" } let(:postal_code) { "75001" } + let(:underage) { "0" } + let(:statutory_representative_email) { nil } let(:extended_data) do { country:, @@ -28,7 +30,9 @@ module Comments gender:, location:, phone_number:, - postal_code: + postal_code:, + underage:, + statutory_representative_email: } end @@ -46,7 +50,9 @@ module Comments "date_of_birth" => date_of_birth, "gender" => gender, "phone_number" => phone_number, - "location" => location + "location" => location, + "underage" => underage, + "statutory_representative_email" => statutory_representative_email } } end @@ -118,7 +124,9 @@ module Comments gender:, location:, phone_number:, - postal_code: + postal_code:, + underage:, + statutory_representative_email: } ).and_call_original @@ -140,6 +148,33 @@ module Comments end.to change(User, :count).by(1) end end + + describe "when the user is underage and sends a valid email" do + let(:underage) { "1" } + let(:statutory_representative_email) { "user@example.fr" } + + it "creates a user with the statutory representative email and sends email" do + expect do + expect(Decidim::ExtraUserFields::StatutoryRepresentativeMailer).to receive(:inform).with(instance_of(Decidim::User)).and_call_original + + command.call + + user = User.last + expect(user.extended_data["statutory_representative_email"]).to eq(statutory_representative_email) + end.to change(User, :count).by(1) + end + end + + describe "when the user is underage and tries to duplicate email" do + let(:underage) { "1" } + let(:statutory_representative_email) { email } + + it "broadcasts invalid" do + expect(Decidim::ExtraUserFields::StatutoryRepresentativeMailer).not_to receive(:inform).with(instance_of(Decidim::User)).and_call_original + + expect { command.call }.to broadcast(:invalid) + end + end end end end diff --git a/spec/commands/decidim/extra_user_fields/admin/update_extra_user_fields_spec.rb b/spec/commands/decidim/extra_user_fields/admin/update_extra_user_fields_spec.rb index 1acb5ba..3631000 100644 --- a/spec/commands/decidim/extra_user_fields/admin/update_extra_user_fields_spec.rb +++ b/spec/commands/decidim/extra_user_fields/admin/update_extra_user_fields_spec.rb @@ -18,6 +18,8 @@ module Admin let(:phone_number_pattern) { "^(\\+34)?[0-9 ]{9,12}$" } let(:phone_number_placeholder) { "+34999888777" } let(:location) { true } + let(:underage) { true } + let(:underage_limit) { 18 } # Block ExtraUserFields RspecVar # EndBlock @@ -34,6 +36,8 @@ module Admin "phone_number_pattern" => phone_number_pattern, "phone_number_placeholder" => phone_number_placeholder, "location" => location, + "underage" => underage, + "underage_limit" => underage_limit, # Block ExtraUserFields ExtraUserFields # EndBlock @@ -86,6 +90,8 @@ module Admin expect(extra_user_fields).to include("country" => { "enabled" => true }) expect(extra_user_fields).to include("phone_number" => { "enabled" => true, "pattern" => phone_number_pattern, "placeholder" => phone_number_placeholder }) expect(extra_user_fields).to include("location" => { "enabled" => true }) + expect(extra_user_fields).to include("underage" => { "enabled" => true }) + expect(extra_user_fields).to include("underage_limit" => 18) # Block ExtraUserFields InclusionSpec # EndBlock diff --git a/spec/forms/decidim/account_form_spec.rb b/spec/forms/decidim/account_form_spec.rb index 1b367c0..ad76c2d 100644 --- a/spec/forms/decidim/account_form_spec.rb +++ b/spec/forms/decidim/account_form_spec.rb @@ -3,6 +3,8 @@ require "spec_helper" module Decidim + include ActiveStorage::Blob::Analyzable + describe AccountForm do subject do described_class.new( @@ -21,7 +23,9 @@ module Decidim date_of_birth:, gender:, phone_number:, - location: + location:, + underage:, + statutory_representative_email: ).with_context( current_organization: organization, current_user: user @@ -38,7 +42,9 @@ module Decidim "date_of_birth" => { "enabled" => true }, "gender" => { "enabled" => true }, "phone_number" => { "enabled" => true, "pattern" => phone_number_pattern, "placeholder" => nil }, - "location" => { "enabled" => true } + "location" => { "enabled" => true }, + "underage" => { "enabled" => true }, + "underage_limit" => 18 } end let(:phone_number_pattern) { "^(\\+34)?[0-9 ]{9,12}$" } @@ -59,6 +65,8 @@ module Decidim let(:location) { "Paris" } let(:phone_number) { "0123456789" } let(:postal_code) { "75001" } + let(:underage) { "0" } + let(:statutory_representative_email) { nil } context "with correct data" do it "is valid" do @@ -75,7 +83,7 @@ module Decidim end context "with invalid phone number format" do - let(:phone_number_pattern) { "^(\\+34)?[0-1 ]{9,12}$" } + let(:phone_number) { "ABCDEFGHIJK" } it "is invalid" do expect(subject).not_to be_valid diff --git a/spec/jobs/decidim/extra_user_fields/admin/export_participants_job_spec.rb b/spec/jobs/decidim/extra_user_fields/admin/export_participants_job_spec.rb new file mode 100644 index 0000000..360facb --- /dev/null +++ b/spec/jobs/decidim/extra_user_fields/admin/export_participants_job_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module ExtraUserFields + module Admin + describe ExportParticipantsJob do + let(:organization) { create(:organization, extra_user_fields: {}) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + let(:format) { "CSV" } + + it "sends an email with a file attached" do + ExportParticipantsJob.perform_now(organization, user, format) + email = last_email + expect(email.subject).to include("participants") + attachment = email.attachments.first + + expect(attachment.read.length).to be_positive + expect(attachment.mime_type).to eq("application/zip") + expect(attachment.filename).to match(/^participants-[0-9]+-[0-9]+-[0-9]+-[0-9]+\.zip$/) + end + + context "when format is CSV" do + it "uses the csv exporter" do + export_data = double + expect(Decidim::Exporters::CSV).to(receive(:new).with(anything, + Decidim::ExtraUserFields::UserExportSerializer)).and_return(double(export: export_data)) + expect(ExportMailer) + .to(receive(:export).with(user, "participants", export_data)) + .and_return(double(deliver_now: true)) + ExportParticipantsJob.perform_now(organization, user, format) + end + end + + context "when format is JSON" do + let(:format) { "JSON" } + + it "uses the json exporter" do + export_data = double + expect(Decidim::Exporters::JSON) + .to(receive(:new).with(anything, Decidim::ExtraUserFields::UserExportSerializer)) + .and_return(double(export: export_data)) + expect(ExportMailer) + .to(receive(:export).with(user, "participants", export_data)) + .and_return(double(deliver_now: true)) + ExportParticipantsJob.perform_now(organization, user, format) + end + end + + context "when format is excel" do + let(:format) { "Excel" } + + it "uses the excel exporter" do + export_data = double + expect(Decidim::Exporters::Excel) + .to(receive(:new).with(anything, Decidim::ExtraUserFields::UserExportSerializer)) + .and_return(double(export: export_data)) + expect(ExportMailer) + .to(receive(:export).with(user, "participants", export_data)) + .and_return(double(deliver_now: true)) + ExportParticipantsJob.perform_now(organization, user, format) + end + end + end + end + end +end diff --git a/spec/serializers/decidim/extra_user_fields/user_export_serializer_spec.rb b/spec/serializers/decidim/extra_user_fields/user_export_serializer_spec.rb index 9eb39bd..f1aa167 100644 --- a/spec/serializers/decidim/extra_user_fields/user_export_serializer_spec.rb +++ b/spec/serializers/decidim/extra_user_fields/user_export_serializer_spec.rb @@ -15,6 +15,8 @@ country:, phone_number:, location:, + underage:, + statutory_representative_email:, # Block ExtraUserFields ExtraUserFields # EndBlock @@ -28,6 +30,9 @@ let(:country) { "Argentina" } let(:phone_number) { "0123456789" } let(:location) { "Cahors" } + let(:underage) { true } + let(:underage_limit) { 18 } + let(:statutory_representative_email) { "parent@example.org" } # Block ExtraUserFields RspecVar # EndBlock diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a30b234..a73f5d2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,3 +7,4 @@ Decidim::Dev.dummy_app_path = File.expand_path(File.join("spec", "decidim_dummy_app")) require "decidim/dev/test/base_spec_helper" + diff --git a/spec/system/account_spec.rb b/spec/system/account_spec.rb index 3033eaa..5c66f68 100644 --- a/spec/system/account_spec.rb +++ b/spec/system/account_spec.rb @@ -10,7 +10,7 @@ end let(:organization) { create(:organization, extra_user_fields:) } - let(:user) { create(:user, :confirmed, organization:, password:, password_confirmation: password) } + let(:user) { create(:user, :confirmed, organization:, password:) } let(:password) { "dqCFgjfDbC7dPbrv" } # rubocop:disable Style/TrailingCommaInHashLiteral let(:extra_user_fields) do @@ -63,15 +63,31 @@ login_as user, scope: :user end + describe "navigation" do + it "shows the account form when clicking on the menu" do + visit decidim.root_path + + within_user_menu do + find("a", text: "account").click + end + + expect(page).to have_css("form.edit_user") + end + end + context "when on the account page" do before do visit decidim.account_path end + it_behaves_like "accessible page" + describe "updating personal data" do + let!(:encrypted_password) { user.encrypted_password } + before do within "form.edit_user" do - select "Castellano", from: :user_locale + select "English", from: :user_locale fill_in :user_name, with: "Nikola Tesla" fill_in :user_personal_url, with: "https://example.org" fill_in :user_about, with: "A Serbian-American inventor, electrical engineer, mechanical engineer, physicist, and futurist." @@ -82,10 +98,6 @@ fill_in :user_postal_code, with: "00000" fill_in :user_phone_number, with: "0123456789" fill_in :user_location, with: "Cahors" - # Block ExtraUserFields FillFieldSpec - - # EndBlock - find("*[type=submit]").click end end @@ -94,6 +106,43 @@ within_flash_messages do expect(page).to have_content("successfully") end + + user.reload + + within_user_menu do + find("a", text: "My public profile").click + end + + expect(page).to have_content("example.org") + expect(page).to have_content("Serbian-American") + + # The user's password should not change when they did not update it + expect(user.reload.encrypted_password).to eq(encrypted_password) + end + + context "when updating avatar" do + it "can update avatar" do + dynamically_attach_file(:user_avatar, Decidim::Dev.asset("avatar.jpg")) + + within "form.edit_user" do + find("*[type=submit]").click + end + + expect(page).to have_css(".flash.success") + end + + it "shows error when image is too big" do + find("#user_avatar_button").click + + within ".upload-modal" do + click_on "Remove" + input_element = find("input[type='file']", visible: :all) + input_element.attach_file(Decidim::Dev.asset("5000x5000.png")) + + expect(page).to have_content("File resolution is too large", count: 1) + expect(page).to have_content("Validation error!") + end + end end context "with phone number pattern blank" do @@ -164,5 +213,360 @@ it_behaves_like "does not display extra user field", "location", "Location" end + + describe "when update password" do + before do + within "form.edit_user" do + select "English", from: :user_locale + fill_in :user_name, with: "Nikola Tesla" + fill_in :user_personal_url, with: "https://example.org" + fill_in :user_about, with: "A Serbian-American inventor, electrical engineer, mechanical engineer, physicist, and futurist." + + fill_in :user_date_of_birth, with: "01/01/2000" + select "Other", from: :user_gender + select "Argentina", from: :user_country + fill_in :user_postal_code, with: "00000" + fill_in :user_phone_number, with: "0123456789" + fill_in :user_location, with: "Cahors" + find("*[type=submit]").click + end + click_on "Change password" + end + + let!(:encrypted_password) { user.encrypted_password } + let(:new_password) { "decidim1234567890" } + + it "toggles old and new password fields" do + within "form.edit_user" do + expect(page).to have_content("must not be too common (e.g. 123456) and must be different from your nickname and your email.") + expect(page).to have_field("user[password]", with: "", type: "password") + expect(page).to have_field("user[old_password]", with: "", type: "password") + click_on "Change password" + expect(page).to have_no_field("user[password]", with: "", type: "password") + expect(page).to have_no_field("user[old_password]", with: "", type: "password") + end + end + + it "shows fields if password is wrong" do + within "form.edit_user" do + fill_in "Password", with: new_password + fill_in "Current password", with: "wrong password12345" + find("*[type=submit]").click + end + expect(page).to have_field("user[password]", with: "decidim1234567890", type: "password") + expect(page).to have_content("is invalid") + end + + it "changes the password with correct password" do + within "form.edit_user" do + fill_in "Password", with: new_password + fill_in "Current password", with: password + find("*[type=submit]").click + end + within_flash_messages do + expect(page).to have_content("successfully") + end + expect(user.reload.encrypted_password).not_to eq(encrypted_password) + expect(page).to have_no_field("user[password]", with: "", type: "password") + expect(page).to have_no_field("user[old_password]", with: "", type: "password") + end + end + + context "when update email" do + let(:pending_email) { "foo@bar.com" } + + before do + within "form.edit_user" do + select "English", from: :user_locale + fill_in :user_name, with: "Nikola Tesla" + fill_in :user_personal_url, with: "https://example.org" + fill_in :user_about, with: "A Serbian-American inventor, electrical engineer, mechanical engineer, physicist, and futurist." + + fill_in :user_date_of_birth, with: "01/01/2000" + select "Other", from: :user_gender + select "Argentina", from: :user_country + fill_in :user_postal_code, with: "00000" + fill_in :user_phone_number, with: "0123456789" + fill_in :user_location, with: "Cahors" + find("*[type=submit]").click + end + end + + context "when typing new email" do + before do + within "form.edit_user" do + fill_in "Your email", with: pending_email + find("*[type=submit]").click + end + end + + it "toggles the current password" do + expect(page).to have_content("In order to confirm the changes to your account, please provide your current password.") + expect(find("#user_old_password")).to be_visible + expect(page).to have_content "Current password" + expect(page).to have_no_content "Password" + end + + it "renders the old password with error" do + within "form.edit_user" do + find("*[type=submit]").click + fill_in :user_old_password, with: "wrong password" + find("*[type=submit]").click + end + within ".flash.alert" do + expect(page).to have_content "There was a problem updating your account." + end + within ".old-user-password" do + expect(page).to have_content "is invalid" + end + end + end + + context "when correct old password" do + before do + within "form.edit_user" do + fill_in "Your email", with: pending_email + find("*[type=submit]").click + fill_in :user_old_password, with: password + + perform_enqueued_jobs { find("*[type=submit]").click } + end + + within_flash_messages do + expect(page).to have_content("You will receive an email to confirm your new email address") + end + # 2 emails generated (confirmation + update) + end + + after do + clear_enqueued_jobs + end + + it "tells user to confirm new email" do + expect(page).to have_content("Email change verification") + expect(page).to have_css("#user_email[disabled='disabled']") + expect(page).to have_content("We have sent an email to #{pending_email} to verify your new email address") + end + + it "resend confirmation" do + within "#email-change-pending" do + click_link_or_button "Send again" + end + expect(page).to have_content("Confirmation email resent successfully to #{pending_email}") + perform_enqueued_jobs + perform_enqueued_jobs + + # the emails include 1 confirmation + 1 update emails added to the 2 previous emails + expect(emails.count).to eq(4) + visit last_email_link + expect(page).to have_content("Your email address has been successfully confirmed") + end + + it "cancels the email change" do + expect(Decidim::User.find(user.id).unconfirmed_email).to eq(pending_email) + within "#email-change-pending" do + click_link_or_button "cancel" + end + + expect(page).to have_content("Email change cancelled successfully") + expect(page).to have_no_content("Email change verification") + expect(Decidim::User.find(user.id).unconfirmed_email).to be_nil + end + end + end + + context "when on the notifications settings page" do + before do + visit decidim.notifications_settings_path + end + + it "updates the user's notifications" do + page.find("[for='newsletter_notifications']").click + + within "form.edit_user" do + find("*[type=submit]").click + end + + within_flash_messages do + expect(page).to have_content("successfully") + end + end + + context "when the user is an admin" do + let!(:user) { create(:user, :confirmed, :admin, password:) } + + before do + login_as user, scope: :user + visit decidim.notifications_settings_path + end + + it "updates the administrator's notifications" do + page.find("[for='email_on_moderations']").click + page.find("[for='user_notification_settings[close_meeting_reminder]']").click + + within "form.edit_user" do + find("*[type=submit]").click + end + + within_flash_messages do + expect(page).to have_content("successfully") + end + end + end + end + + context "when on the interests page" do + before do + visit decidim.user_interests_path + end + + it "does not find any scopes" do + expect(page).to have_content("My interests") + expect(page).to have_content("This organization does not have any scope yet") + end + + context "when scopes are defined" do + let!(:scopes) { create_list(:scope, 3, organization:) } + let!(:subscopes) { create_list(:subscope, 3, parent: scopes.first) } + + before do + visit decidim.user_interests_path + end + + it "display translated scope name" do + expect(page).to have_content("My interests") + within "label[for='user_scopes_#{scopes.first.id}_checked']" do + expect(page).to have_content(translated(scopes.first.name)) + end + end + + it "allows to choose interests" do + label_field = "label[for='user_scopes_#{scopes.first.id}_checked']" + expect(page).to have_content("My interests") + find(label_field).click + click_on "Update my interests" + + within_flash_messages do + expect(page).to have_content("Your interests have been successfully updated.") + end + end + end + end + + context "when on the delete my account page" do + before do + visit decidim.delete_account_path + end + + it "does not display the authorizations message by default" do + expect(page).to have_no_content("Some data bound to your authorization will be saved for security.") + end + + it "the user can delete their account" do + fill_in :delete_user_delete_account_delete_reason, with: "I just want to delete my account" + + within ".form__wrapper-block" do + click_on "Delete my account" + end + + click_on "Yes, I want to delete my account" + + within_flash_messages do + expect(page).to have_content("successfully") + end + + click_link_or_button("Log in", match: :first) + + within ".new_user" do + fill_in :session_user_email, with: user.email + fill_in :session_user_password, with: password + find("*[type=submit]").click + end + + expect(page).to have_no_content("Signed in successfully") + expect(page).to have_no_content(user.name) + end + + context "when the user has an authorization" do + let!(:authorization) { create(:authorization, :granted, user:) } + + it "displays the authorizations message" do + visit decidim.delete_account_path + + expect(page).to have_content("Some data bound to your authorization will be saved for security.") + end + end + end + end + + context "when on the notifications page in a PWA browser" do + let(:organization) { create(:organization, host: "pwa.lvh.me") } + let(:user) { create(:user, :confirmed, password:, organization:) } + let(:password) { "dqCFgjfDbC7dPbrv" } + let(:vapid_keys) do + { + enabled: true, + public_key: "BKmjw_A8tJCcZNQ72uG8QW15XHQnrGJjHjsmoUILUUFXJ1VNhOnJLc3ywR3eZKibX4HSqhB1hAzZFj__3VqzcPQ=", + private_key: "TF_MRbSSs_4BE1jVfOsILSJemND8cRMpiznWHgdsro0=" + } + end + + context "when VAPID keys are set" do + before do + Rails.application.secrets[:vapid] = vapid_keys + driven_by(:pwa_chrome) + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim.notifications_settings_path + end + + context "when on the account page" do + it "enables push notifications if supported browser" do + sleep 2 + page.find("[for='allow_push_notifications']").click + + # Wait for the browser to be subscribed + sleep 5 + + within "form.edit_user" do + find("*[type=submit]").click + end + + within_flash_messages do + expect(page).to have_content("successfully") + end + + find(:css, "#allow_push_notifications", visible: false).execute_script("this.checked = true") + end + end + end + + context "when VAPID is disabled" do + before do + Rails.application.secrets[:vapid] = { enabled: false } + driven_by(:pwa_chrome) + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim.notifications_settings_path + end + + it "does not show the push notifications switch" do + expect(page).to have_no_selector(".push-notifications") + end + end + + context "when VAPID keys are not set" do + before do + Rails.application.secrets.delete(:vapid) + driven_by(:pwa_chrome) + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim.notifications_settings_path + end + + it "does not show the push notifications switch" do + expect(page).to have_no_selector(".push-notifications") + end + end end end diff --git a/spec/system/admin_manages_officializations_spec.rb b/spec/system/admin_manages_officializations_spec.rb index 3c9f367..93a1b69 100644 --- a/spec/system/admin_manages_officializations_spec.rb +++ b/spec/system/admin_manages_officializations_spec.rb @@ -19,13 +19,25 @@ within ".layout-nav" do click_on "Participants" end - end - - it "includes export dropdown button" do within ".sidebar-menu" do click_on "Participants" end + end + it "includes export dropdown button" do expect(page).to have_content("Export") end + + context "when clicking on export csv button" do + before do + find("span.exports").click + click_on "Export CSV" + end + + it "redirects to officialization index page and display a flash message" do + expect(page).to have_title("Participants") + expect(page).to have_content("Export") + expect(page).to have_content("Your export is currently in progress. You will receive an email when it is complete") + end + end end