diff --git a/.erb-lint.yml b/.erb-lint.yml
new file mode 100644
index 0000000000..3321adde4e
--- /dev/null
+++ b/.erb-lint.yml
@@ -0,0 +1,30 @@
+---
+
+linters:
+ ExtraNewline:
+ enabled: true
+
+ FinalNewline:
+ enabled: true
+
+ SpaceAroundErbTag:
+ enabled: true
+
+ AllowedScriptType:
+ enabled: true
+ allowed_types:
+ - text/javascript
+ - text/template
+
+ Rubocop:
+ enabled: true
+
+ rubocop_config:
+ AllCops:
+ DisabledByDefault: true
+
+ Style/StringLiterals:
+ EnforcedStyle: double_quotes
+
+ Layout/SpaceInsideHashLiteralBraces:
+ Enabled: true
diff --git a/.github/run_erblint.sh b/.github/run_erblint.sh
new file mode 100755
index 0000000000..9597e4a780
--- /dev/null
+++ b/.github/run_erblint.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+shopt -s globstar
+
+bundle exec erblint app/{cells,views}/**/*.erb
+
+# Store the return code of the erblint execution
+EXIT_CODE=$?
+
+shopt -u globstar
+
+exit $EXIT_CODE
diff --git a/.github/upload_coverage.sh b/.github/upload_coverage.sh
new file mode 100755
index 0000000000..7624a57ee6
--- /dev/null
+++ b/.github/upload_coverage.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+REPORT_NAME=$1
+EVENT_PAYLOAD_FILE=$2
+
+PRID=`jq ".number // .check_run.pull_requests[0].number" $EVENT_PAYLOAD_FILE`
+SHA=`jq -r ".pull_request.head.sha // .check_run.head_sha // .after" $EVENT_PAYLOAD_FILE`
+
+if [ $PRID = "null" ]
+then
+ bash <(curl -s https://codecov.io/bash) -n $REPORT_NAME -C $SHA
+else
+ bash <(curl -s https://codecov.io/bash) -n $REPORT_NAME -C $SHA -P $PRID
+fi
diff --git a/.github/workflows/lint_code.yml b/.github/workflows/lint_code.yml
new file mode 100644
index 0000000000..c8e7d8e935
--- /dev/null
+++ b/.github/workflows/lint_code.yml
@@ -0,0 +1,51 @@
+name: "[CI] Lint"
+on:
+ push:
+ branches:
+ - develop
+ - release/*
+ - "*-stable"
+ pull_request:
+ branches-ignore:
+ - "chore/l10n*"
+
+env:
+ CI: "true"
+ SIMPLECOV: "true"
+ RUBY_VERSION: 2.7.1
+
+jobs:
+ lint:
+ name: Lint code
+ runs-on: ubuntu-latest
+ if: "!startsWith(github.head_ref, 'chore/l10n')"
+ timeout-minutes: 60
+ 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:
+ ruby-version: ${{ env.RUBY_VERSION }}
+ 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)-lint"
+ - 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-
+ - name: Install JS dependencies
+ run: npm ci
+ - run: bundle exec rubocop -P
+ name: Lint Ruby files
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000000..309e12409d
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,68 @@
+name: Tests
+on:
+ push:
+ branches:
+ - develop
+ - master
+ - release/*
+ - "*-stable"
+ pull_request:
+ branches-ignore:
+ - "chore/l10n*"
+
+env:
+ CI: "true"
+ SIMPLECOV: "true"
+ RUBY_VERSION: 2.7.1
+
+jobs:
+ tests:
+ name: Tests
+ runs-on: ubuntu-latest
+ if: "!startsWith(github.head_ref, 'chore/l10n')"
+ 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
+ RAILS_ENV: test
+ # Set locales available for i18n tasks
+ ENFORCED_LOCALES: "en,fr"
+ steps:
+ - uses: rokroskar/workflow-run-cleanup-action@v0.2.2
+ if: "github.ref != 'refs/heads/master' || 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:
+ ruby-version: ${{ env.RUBY_VERSION }}
+ bundler-cache: true
+ - name: Create db
+ run: |
+ bundle exec rails db:create
+ bundle exec rails db:migrate
+ - run: mkdir -p ./spec/tmp/screenshots
+ name: Create the screenshots folder
+ - uses: nanasess/setup-chromedriver@v1.0.1
+ - run: bundle exec rspec
+ name: RSpec
+ - run: ./.github/upload_coverage.sh $DECIDIM_MODULE $GITHUB_EVENT_PATH
+ name: Upload coverage
+ - uses: actions/upload-artifact@v2-preview
+ if: always()
+ with:
+ name: screenshots
+ path: ./spec/decidim_dummy_app/tmp/screenshots
diff --git a/.node-version b/.node-version
new file mode 100644
index 0000000000..fc2cbe5027
--- /dev/null
+++ b/.node-version
@@ -0,0 +1 @@
+15.14.0
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000000..ea7414f855
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,1250 @@
+require: rubocop-rspec
+
+# Common configuration.
+AllCops:
+ NewCops: enable
+ Include:
+ - .simplecov
+ - "**/*.rb"
+ - "**/*.rake"
+ - "**/*.gemspec"
+ - "**/*.ru"
+ - "**/Gemfile"
+ - "**/Rakefile"
+ Exclude:
+ - "**/vendor/**/*"
+ - "db/migrate/*.rb"
+ - "db/schema.rb"
+ - "bin/*"
+ - "development_app/**/*"
+ - "spec/decidim_dummy_app/**/*"
+ - "node_modules/**/*"
+ - "test/**/*"
+ # Default formatter will be used if no -f/--format option is given.
+ DefaultFormatter: progress
+ # Cop names are not displayed in offense messages by default. Change behavior
+ # by overriding DisplayCopNames, or by giving the -D/--display-cop-names
+ # option.
+ DisplayCopNames: true
+ # Style guide URLs are not displayed in offense messages by default. Change
+ # behavior by overriding DisplayStyleGuide, or by giving the
+ # -S/--display-style-guide option.
+ DisplayStyleGuide: false
+ # Extra details are not displayed in offense messages by default. Change
+ # behavior by overriding ExtraDetails, or by giving the
+ # -E/--extra-details option.
+ ExtraDetails: false
+ # Additional cops that do not reference a style guide rule may be enabled by
+ # default. Change behavior by overriding StyleGuideCopsOnly, or by giving
+ # the --only-guide-cops option.
+ StyleGuideCopsOnly: false
+ # All cops except the ones in disabled.yml are enabled by default. Change
+ # this behavior by overriding DisabledByDefault. When DisabledByDefault is
+ # true, all cops in the default configuration are disabled, and and only cops
+ # in user configuration are enabled. This makes cops opt-in instead of
+ # opt-out. Note that when DisabledByDefault is true, cops in user
+ # configuration will be enabled even if they don't set the Enabled parameter.
+ DisabledByDefault: false
+ # Enables the result cache if true. Can be overridden by the --cache command
+ # line option.
+ UseCache: true
+ # Threshold for how many files can be stored in the result cache before some
+ # of the files are automatically removed.
+ MaxFilesInCache: 20000
+ # The cache will be stored in "rubocop_cache" under this directory. The name
+ # "/tmp" is special and will be converted to the system temporary directory,
+ # which is "/tmp" on Unix-like systems, but could be something else on other
+ # systems.
+ CacheRootDirectory: /tmp
+ # The default cache root directory is /tmp, which on most systems is
+ # writable by any system user. This means that it is possible for a
+ # malicious user to anticipate the location of Rubocop's cache directory,
+ # and create a symlink in its place that could cause Rubocop to overwrite
+ # unintended files, or read malicious input. If you are certain that your
+ # cache location is secure from this kind of attack, and wish to use a
+ # symlinked cache location, set this value to "true".
+ AllowSymlinksInCacheRootDirectory: true
+ # What MRI version of the Ruby interpreter is the inspected code intended to
+ # run on? (If there is more than one, set this to the lowest version.)
+ # If a value is specified for TargetRubyVersion then it is used.
+ # Else if .ruby-version exists and it contains an MRI version it is used.
+ # Otherwise we fallback to the oldest officially supported Ruby version (2.0).
+ TargetRubyVersion: 2.5
+
+ RSpec:
+ Patterns:
+ - "(?:^|/)spec/"
+ - "(?:^|/)test/"
+
+# Indent private/protected/public as deep as method definitions
+Layout/AccessModifierIndentation:
+ EnforcedStyle: indent
+ SupportedStyles:
+ - outdent
+ - indent
+ # By default, the indentation width from Style/IndentationWidth is used
+ # But it can be overridden by setting this parameter
+ IndentationWidth: ~
+
+Style/Alias:
+ EnforcedStyle: prefer_alias
+ SupportedStyles:
+ - prefer_alias
+ - prefer_alias_method
+
+# Align the elements of a hash literal if they span more than one line.
+Layout/HashAlignment:
+ # Alignment of entries using hash rocket as separator. Valid values are:
+ #
+ # key - left alignment of keys
+ # "a" => 2
+ # "bb" => 3
+ # separator - alignment of hash rockets, keys are right aligned
+ # "a" => 2
+ # "bb" => 3
+ # table - left alignment of keys, hash rockets, and values
+ # "a" => 2
+ # "bb" => 3
+ EnforcedHashRocketStyle: key
+ # Alignment of entries using colon as separator. Valid values are:
+ #
+ # key - left alignment of keys
+ # a: 0
+ # bb: 1
+ # separator - alignment of colons, keys are right aligned
+ # a: 0
+ # bb: 1
+ # table - left alignment of keys and values
+ # a: 0
+ # bb: 1
+ EnforcedColonStyle: key
+ # Select whether hashes that are the last argument in a method call should be
+ # inspected? Valid values are:
+ #
+ # always_inspect - Inspect both implicit and explicit hashes.
+ # Registers an offense for:
+ # function(a: 1,
+ # b: 2)
+ # Registers an offense for:
+ # function({a: 1,
+ # b: 2})
+ # always_ignore - Ignore both implicit and explicit hashes.
+ # Accepts:
+ # function(a: 1,
+ # b: 2)
+ # Accepts:
+ # function({a: 1,
+ # b: 2})
+ # ignore_implicit - Ignore only implicit hashes.
+ # Accepts:
+ # function(a: 1,
+ # b: 2)
+ # Registers an offense for:
+ # function({a: 1,
+ # b: 2})
+ # ignore_explicit - Ignore only explicit hashes.
+ # Accepts:
+ # function({a: 1,
+ # b: 2})
+ # Registers an offense for:
+ # function(a: 1,
+ # b: 2)
+ EnforcedLastArgumentHashStyle: always_inspect
+ SupportedLastArgumentHashStyles:
+ - always_inspect
+ - always_ignore
+ - ignore_implicit
+ - ignore_explicit
+
+Layout/ParameterAlignment:
+ # Alignment of parameters in multi-line method calls.
+ #
+ # The `with_first_parameter` style aligns the following lines along the same
+ # column as the first parameter.
+ #
+ # method_call(a,
+ # b)
+ #
+ # The `with_fixed_indentation` style aligns the following lines with one
+ # level of indentation relative to the start of the line with the method call.
+ #
+ # method_call(a,
+ # b)
+ EnforcedStyle: with_first_parameter
+ SupportedStyles:
+ - with_first_parameter
+ - with_fixed_indentation
+ # By default, the indentation width from Style/IndentationWidth is used
+ # But it can be overridden by setting this parameter
+ IndentationWidth: ~
+
+Style/AndOr:
+ # Whether `and` and `or` are banned only in conditionals (conditionals)
+ # or completely (always).
+ EnforcedStyle: always
+ SupportedStyles:
+ - always
+ - conditionals
+
+Style/AsciiComments:
+ Enabled: false
+
+# Checks if usage of %() or %Q() matches configuration.
+Style/BarePercentLiterals:
+ EnforcedStyle: bare_percent
+ SupportedStyles:
+ - percent_q
+ - bare_percent
+
+Style/BlockDelimiters:
+ EnforcedStyle: line_count_based
+ SupportedStyles:
+ # The `line_count_based` style enforces braces around single line blocks and
+ # do..end around multi-line blocks.
+ - line_count_based
+ # The `semantic` style enforces braces around functional blocks, where the
+ # primary purpose of the block is to return a value and do..end for
+ # procedural blocks, where the primary purpose of the block is its
+ # side-effects.
+ #
+ # This looks at the usage of a block's method to determine its type (e.g. is
+ # the result of a `map` assigned to a variable or passed to another
+ # method) but exceptions are permitted in the `ProceduralMethods`,
+ # `FunctionalMethods` and `IgnoredMethods` sections below.
+ - semantic
+ # The `braces_for_chaining` style enforces braces around single line blocks
+ # and do..end around multi-line blocks, except for multi-line blocks whose
+ # return value is being chained with another method (in which case braces
+ # are enforced).
+ - braces_for_chaining
+ ProceduralMethods:
+ # Methods that are known to be procedural in nature but look functional from
+ # their usage, e.g.
+ #
+ # time = Benchmark.realtime do
+ # foo.bar
+ # end
+ #
+ # Here, the return value of the block is discarded but the return value of
+ # `Benchmark.realtime` is used.
+ - benchmark
+ - bm
+ - bmbm
+ - create
+ - each_with_object
+ - measure
+ - new
+ - realtime
+ - tap
+ - with_object
+ FunctionalMethods:
+ # Methods that are known to be functional in nature but look procedural from
+ # their usage, e.g.
+ #
+ # let(:foo) { Foo.new }
+ #
+ # Here, the return value of `Foo.new` is used to define a `foo` helper but
+ # doesn't appear to be used from the return value of `let`.
+ - let
+ - let!
+ - subject
+ - watch
+ IgnoredMethods:
+ # Methods that can be either procedural or functional and cannot be
+ # categorised from their usage alone, e.g.
+ #
+ # foo = lambda do |x|
+ # puts "Hello, #{x}"
+ # end
+ #
+ # foo = lambda do |x|
+ # x * 100
+ # end
+ #
+ # Here, it is impossible to tell from the return value of `lambda` whether
+ # the inner block's return value is significant.
+ - lambda
+ - proc
+ - it
+
+# Indentation of `when`.
+Layout/CaseIndentation:
+ EnforcedStyle: case
+ SupportedStyles:
+ - case
+ - end
+ IndentOneStep: false
+ # By default, the indentation width from Style/IndentationWidth is used
+ # But it can be overridden by setting this parameter
+ # This only matters if IndentOneStep is true
+ IndentationWidth: ~
+
+Style/ClassAndModuleChildren:
+ Enabled: false
+ # Checks the style of children definitions at classes and modules.
+ #
+ # Basically there are two different styles:
+ #
+ # `nested` - have each child on a separate line
+ # class Foo
+ # class Bar
+ # end
+ # end
+ #
+ # `compact` - combine definitions as much as possible
+ # class Foo::Bar
+ # end
+ #
+ # The compact style is only forced, for classes / modules with one child.
+ EnforcedStyle: nested
+ SupportedStyles:
+ - nested
+ - compact
+
+Style/ClassCheck:
+ EnforcedStyle: is_a?
+ SupportedStyles:
+ - is_a?
+ - kind_of?
+
+# Align with the style guide.
+Style/CollectionMethods:
+ # Mapping from undesired method to desired_method
+ # e.g. to use `detect` over `find`:
+ #
+ # CollectionMethods:
+ # PreferredMethods:
+ # find: detect
+ PreferredMethods:
+ collect: "map"
+ collect!: "map!"
+ inject: "reduce"
+ detect: "find"
+ find_all: "select"
+
+# Use ` or %x around command literals.
+Style/CommandLiteral:
+ EnforcedStyle: backticks
+ # backticks: Always use backticks.
+ # percent_x: Always use %x.
+ # mixed: Use backticks on single-line commands, and %x on multi-line commands.
+ SupportedStyles:
+ - backticks
+ - percent_x
+ - mixed
+ # If false, the cop will always recommend using %x if one or more backticks
+ # are found in the command string.
+ AllowInnerBackticks: false
+
+# Checks formatting of special comments
+Style/CommentAnnotation:
+ Keywords:
+ - TODO
+ - FIXME
+ - OPTIMIZE
+ - HACK
+ - REVIEW
+
+Style/ConditionalAssignment:
+ EnforcedStyle: assign_to_condition
+ SupportedStyles:
+ - assign_to_condition
+ - assign_inside_condition
+ # When configured to `assign_to_condition`, `SingleLineConditionsOnly`
+ # will only register an offense when all branches of a condition are
+ # a single line.
+ # When configured to `assign_inside_condition`, `SingleLineConditionsOnly`
+ # will only register an offense for assignment to a condition that has
+ # at least one multiline branch.
+ SingleLineConditionsOnly: true
+
+# Checks that you have put a copyright in a comment before any code.
+#
+# You can override the default Notice in your .rubocop.yml file.
+#
+# In order to use autocorrect, you must supply a value for the
+# AutocorrectNotice key that matches the regexp Notice. A blank
+# AutocorrectNotice will cause an error during autocorrect.
+#
+# Autocorrect will add a copyright notice in a comment at the top
+# of the file immediately after any shebang or encoding comments.
+#
+# Example rubocop.yml:
+#
+# Style/Copyright:
+# Enabled: true
+# Notice: 'Copyright (\(c\) )?2015 Yahoo! Inc'
+# AutocorrectNotice: "# Copyright (c) 2015 Yahoo! Inc."
+#
+Style/Copyright:
+ Notice: '^Copyright (\(c\) )?2[0-9]{3} .+'
+ AutocorrectNotice: ""
+
+Style/DocumentationMethod:
+ RequireForNonPublicMethods: false
+
+# Multi-line method chaining should be done with leading dots.
+Layout/DotPosition:
+ EnforcedStyle: leading
+ SupportedStyles:
+ - leading
+ - trailing
+
+# Warn on empty else statements
+# empty - warn only on empty else
+# nil - warn on else with nil in it
+# both - warn on empty else and else with nil in it
+Style/EmptyElse:
+ EnforcedStyle: both
+ SupportedStyles:
+ - empty
+ - nil
+ - both
+
+# Use empty lines between defs.
+Layout/EmptyLineBetweenDefs:
+ # If true, this parameter means that single line method definitions don't
+ # need an empty line between them.
+ AllowAdjacentOneLineDefs: false
+
+Layout/EmptyLinesAroundBlockBody:
+ EnforcedStyle: no_empty_lines
+ SupportedStyles:
+ - empty_lines
+ - no_empty_lines
+
+Layout/EmptyLinesAroundClassBody:
+ EnforcedStyle: no_empty_lines
+ SupportedStyles:
+ - empty_lines
+ - no_empty_lines
+
+Layout/EmptyLinesAroundModuleBody:
+ EnforcedStyle: no_empty_lines
+ SupportedStyles:
+ - empty_lines
+ - no_empty_lines
+
+# Checks whether the source file has a utf-8 encoding comment or not
+# AutoCorrectEncodingComment must match the regex
+# /#.*coding\s?[:=]\s?(?:UTF|utf)-8/
+Style/Encoding:
+ Enabled: true
+
+Layout/ExtraSpacing:
+ # When true, allows most uses of extra spacing if the intent is to align
+ # things with the previous or next line, not counting empty lines or comment
+ # lines.
+ AllowForAlignment: false
+ # When true, forces the alignment of = in assignments on consecutive lines.
+ ForceEqualSignAlignment: false
+
+Naming/FileName:
+ Exclude:
+ - "**/Gemfile"
+ - "**/Rakefile"
+ - "**/*.gemspec"
+ # When true, requires that each source file should define a class or module
+ # with a name which matches the file name (converted to ... case).
+ # It further expects it to be nested inside modules which match the names
+ # of subdirectories in its path.
+ ExpectMatchingDefinition: false
+ # If non-nil, expect all source file names to match the following regex.
+ # Only the file name itself is matched, not the entire file path.
+ # Use anchors as necessary if you want to match the entire name rather than
+ # just a part of it.
+ Regex: ~
+ # With `IgnoreExecutableScripts` set to `true`, this cop does not
+ # report offending filenames for executable scripts (i.e. source
+ # files with a shebang in the first line).
+ IgnoreExecutableScripts: true
+
+Layout/FirstArgumentIndentation:
+ EnforcedStyle: special_for_inner_method_call_in_parentheses
+ SupportedStyles:
+ # The first parameter should always be indented one step more than the
+ # preceding line.
+ - consistent
+ # The first parameter should normally be indented one step more than the
+ # preceding line, but if it's a parameter for a method call that is itself
+ # a parameter in a method call, then the inner parameter should be indented
+ # relative to the inner method.
+ - special_for_inner_method_call
+ # Same as special_for_inner_method_call except that the special rule only
+ # applies if the outer method call encloses its arguments in parentheses.
+ - special_for_inner_method_call_in_parentheses
+ # By default, the indentation width from Style/IndentationWidth is used
+ # But it can be overridden by setting this parameter
+ IndentationWidth: ~
+
+# Checks use of for or each in multiline loops.
+Style/For:
+ EnforcedStyle: each
+ SupportedStyles:
+ - for
+ - each
+
+# Enforce the method used for string formatting.
+Style/FormatString:
+ EnforcedStyle: format
+ SupportedStyles:
+ - format
+ - sprintf
+ - percent
+
+Style/FormatStringToken:
+ EnforcedStyle: template
+
+Style/FrozenStringLiteralComment:
+ EnforcedStyle: always
+ SupportedStyles:
+ - never
+ # `always` will always add the frozen string literal comment to a file
+ # regardless of the Ruby version or if `freeze` or `<<` are called on a
+ # string literal. If you run code against multiple versions of Ruby, it is
+ # possible that this will create errors in Ruby 2.3.0+.
+ - always
+
+# Built-in global variables are allowed by default.
+Style/GlobalVars:
+ AllowedVariables: []
+
+# `MinBodyLength` defines the number of lines of the a body of an if / unless
+# needs to have to trigger this cop
+Style/GuardClause:
+ MinBodyLength: 6
+
+Style/HashSyntax:
+ EnforcedStyle: ruby19
+ SupportedStyles:
+ # checks for 1.9 syntax (e.g. {a: 1}) for all symbol keys
+ - ruby19
+ # checks for hash rocket syntax for all hashes
+ - hash_rockets
+ # forbids mixed key syntaxes (e.g. {a: 1, :b => 2})
+ - no_mixed_keys
+ # enforces both ruby19 and no_mixed_keys styles
+ - ruby19_no_mixed_keys
+ # Force hashes that have a symbol value to use hash rockets
+ UseHashRocketsWithSymbolValues: false
+ # Do not suggest { a?: 1 } over { :a? => 1 } in ruby19 style
+ PreferHashRocketsForNonAlnumEndingSymbols: false
+
+Layout/IndentationConsistency:
+ # The difference between `rails` and `normal` is that the `rails` style
+ # prescribes that in classes and modules the `protected` and `private`
+ # modifier keywords shall be indented the same as public methods and that
+ # protected and private members shall be indented one step more than the
+ # modifiers. Other than that, both styles mean that entities on the same
+ # logical depth shall have the same indentation.
+ EnforcedStyle: normal
+ SupportedStyles:
+ - normal
+ - rails
+
+Layout/IndentationWidth:
+ # Number of spaces for each indentation level.
+ Width: 2
+
+# Checks the indentation of the first element in an array literal.
+Layout/FirstArrayElementIndentation:
+ # The value `special_inside_parentheses` means that array literals with
+ # brackets that have their opening bracket on the same line as a surrounding
+ # opening round parenthesis, shall have their first element indented relative
+ # to the first position inside the parenthesis.
+ #
+ # The value `consistent` means that the indentation of the first element shall
+ # always be relative to the first position of the line where the opening
+ # bracket is.
+ #
+ # The value `align_brackets` means that the indentation of the first element
+ # shall always be relative to the position of the opening bracket.
+ EnforcedStyle: special_inside_parentheses
+ SupportedStyles:
+ - special_inside_parentheses
+ - consistent
+ - align_brackets
+ # By default, the indentation width from Style/IndentationWidth is used
+ # But it can be overridden by setting this parameter
+ IndentationWidth: ~
+
+# Checks the indentation of assignment RHS, when on a different line from LHS
+Layout/AssignmentIndentation:
+ # By default, the indentation width from Style/IndentationWidth is used
+ # But it can be overridden by setting this parameter
+ IndentationWidth: ~
+
+# Checks the indentation of the first key in a hash literal.
+Layout/FirstHashElementIndentation:
+ # The value `special_inside_parentheses` means that hash literals with braces
+ # that have their opening brace on the same line as a surrounding opening
+ # round parenthesis, shall have their first key indented relative to the
+ # first position inside the parenthesis.
+ #
+ # The value `consistent` means that the indentation of the first key shall
+ # always be relative to the first position of the line where the opening
+ # brace is.
+ #
+ # The value `align_braces` means that the indentation of the first key shall
+ # always be relative to the position of the opening brace.
+ EnforcedStyle: special_inside_parentheses
+ SupportedStyles:
+ - special_inside_parentheses
+ - consistent
+ - align_braces
+ # By default, the indentation width from Style/IndentationWidth is used
+ # But it can be overridden by setting this parameter
+ IndentationWidth: ~
+
+Style/Lambda:
+ EnforcedStyle: line_count_dependent
+ SupportedStyles:
+ - line_count_dependent
+ - lambda
+ - literal
+ Exclude:
+ - "**/types/**/*"
+ - "**/*_interface.rb"
+
+Style/LambdaCall:
+ EnforcedStyle: call
+ SupportedStyles:
+ - call
+ - braces
+
+Style/Next:
+ # With `always` all conditions at the end of an iteration needs to be
+ # replaced by next - with `skip_modifier_ifs` the modifier if like this one
+ # are ignored: [1, 2].each { |a| return "yes" if a == 1 }
+ EnforcedStyle: skip_modifier_ifs
+ # `MinBodyLength` defines the number of lines of the a body of an if / unless
+ # needs to have to trigger this cop
+ MinBodyLength: 3
+ SupportedStyles:
+ - skip_modifier_ifs
+ - always
+
+Style/NonNilCheck:
+ # With `IncludeSemanticChanges` set to `true`, this cop reports offenses for
+ # `!x.nil?` and autocorrects that and `x != nil` to solely `x`, which is
+ # **usually** OK, but might change behavior.
+ #
+ # With `IncludeSemanticChanges` set to `false`, this cop does not report
+ # offenses for `!x.nil?` and does no changes that might change behavior.
+ IncludeSemanticChanges: false
+
+Style/NumericPredicate:
+ EnforcedStyle: predicate
+ SupportedStyles:
+ - predicate
+ - comparison
+
+Style/MethodDefParentheses:
+ EnforcedStyle: require_parentheses
+ SupportedStyles:
+ - require_parentheses
+ - require_no_parentheses
+ - require_no_parentheses_except_multiline
+
+Naming/MethodName:
+ EnforcedStyle: snake_case
+ SupportedStyles:
+ - snake_case
+ - camelCase
+
+Style/ModuleFunction:
+ EnforcedStyle: module_function
+ SupportedStyles:
+ - module_function
+ - extend_self
+
+Layout/MultilineArrayBraceLayout:
+ EnforcedStyle: symmetrical
+ SupportedStyles:
+ # symmetrical: closing brace is positioned in same way as opening brace
+ # new_line: closing brace is always on a new line
+ # same_line: closing brace is always on the same line as last element
+ - symmetrical
+ - new_line
+ - same_line
+
+Layout/MultilineAssignmentLayout:
+ # The types of assignments which are subject to this rule.
+ SupportedTypes:
+ - block
+ - case
+ - class
+ - if
+ - kwbegin
+ - module
+ EnforcedStyle: new_line
+ SupportedStyles:
+ # Ensures that the assignment operator and the rhs are on the same line for
+ # the set of supported types.
+ - same_line
+ # Ensures that the assignment operator and the rhs are on separate lines
+ # for the set of supported types.
+ - new_line
+
+Layout/MultilineHashBraceLayout:
+ EnforcedStyle: symmetrical
+ SupportedStyles:
+ # symmetrical: closing brace is positioned in same way as opening brace
+ # new_line: closing brace is always on a new line
+ # same_line: closing brace is always on same line as last element
+ - symmetrical
+ - new_line
+ - same_line
+
+Layout/MultilineMethodCallBraceLayout:
+ EnforcedStyle: symmetrical
+ SupportedStyles:
+ # symmetrical: closing brace is positioned in same way as opening brace
+ # new_line: closing brace is always on a new line
+ # same_line: closing brace is always on the same line as last argument
+ - symmetrical
+ - new_line
+ - same_line
+
+Layout/MultilineMethodCallIndentation:
+ EnforcedStyle: aligned
+ SupportedStyles:
+ - aligned
+ - indented
+ - indented_relative_to_receiver
+ # By default, the indentation width from Style/IndentationWidth is used
+ # But it can be overridden by setting this parameter
+ IndentationWidth: ~
+
+Layout/MultilineMethodDefinitionBraceLayout:
+ EnforcedStyle: symmetrical
+ SupportedStyles:
+ # symmetrical: closing brace is positioned in same way as opening brace
+ # new_line: closing brace is always on a new line
+ # same_line: closing brace is always on the same line as last parameter
+ - symmetrical
+ - new_line
+ - same_line
+
+Layout/MultilineOperationIndentation:
+ EnforcedStyle: aligned
+ SupportedStyles:
+ - aligned
+ - indented
+ # By default, the indentation width from Style/IndentationWidth is used
+ # But it can be overridden by setting this parameter
+ IndentationWidth: ~
+
+Style/NumericLiterals:
+ MinDigits: 5
+
+Style/NumericLiteralPrefix:
+ EnforcedOctalStyle: zero_with_o
+ SupportedOctalStyles:
+ - zero_with_o
+ - zero_only
+
+Style/OptionHash:
+ # A list of parameter names that will be flagged by this cop.
+ SuspiciousParamNames:
+ - options
+ - opts
+ - args
+ - params
+ - parameters
+
+# Allow safe assignment in conditions.
+Style/ParenthesesAroundCondition:
+ AllowSafeAssignment: true
+
+Style/PercentLiteralDelimiters:
+ PreferredDelimiters:
+ "%": ()
+ "%i": ()
+ "%q": ()
+ "%Q": ()
+ "%r": "{}"
+ "%s": ()
+ "%w": ()
+ "%W": ()
+ "%x": ()
+
+Style/PercentQLiterals:
+ EnforcedStyle: lower_case_q
+ SupportedStyles:
+ - lower_case_q # Use %q when possible, %Q when necessary
+ - upper_case_q # Always use %Q
+
+Naming/PredicateName:
+ # Predicate name prefixes.
+ NamePrefix:
+ - is_
+ - has_
+ - have_
+ # Predicate name prefixes that should be removed.
+ ForbiddenPrefixes:
+ - is_
+ - have_
+ # Predicate names which, despite having a blacklisted prefix, or no ?,
+ # should still be accepted
+ AllowedMethods:
+ - is_a?
+ # Exclude Rspec specs because there is a strong convetion to write spec
+ # helpers in the form of `have_something` or `be_something`.
+ Exclude:
+ - "**/spec/**/*"
+ - "**/test/**/*"
+
+Style/PreferredHashMethods:
+ Enabled: true
+ EnforcedStyle: verbose
+
+Style/DateTime:
+ Enabled: true
+
+Style/Documentation:
+ Enabled: false
+
+Style/RaiseArgs:
+ EnforcedStyle: exploded
+ SupportedStyles:
+ - compact # raise Exception.new(msg)
+ - exploded # raise Exception, msg
+
+Style/RedundantReturn:
+ # When true allows code like `return x, y`.
+ AllowMultipleReturnValues: false
+
+# Use / or %r around regular expressions.
+Style/RegexpLiteral:
+ EnforcedStyle: slashes
+ # slashes: Always use slashes.
+ # percent_r: Always use %r.
+ # mixed: Use slashes on single-line regexes, and %r on multi-line regexes.
+ SupportedStyles:
+ - slashes
+ - percent_r
+ - mixed
+ # If false, the cop will always recommend using %r if one or more slashes
+ # are found in the regexp string.
+ AllowInnerSlashes: false
+
+Style/SafeNavigation:
+ Enabled: false
+
+Style/Semicolon:
+ # Allow ; to separate several expressions on the same line.
+ AllowAsExpressionSeparator: false
+
+Style/SignalException:
+ EnforcedStyle: only_raise
+ SupportedStyles:
+ - only_raise
+ - only_fail
+ - semantic
+
+Style/SingleLineBlockParams:
+ Methods:
+ - reduce:
+ - a
+ - e
+ - inject:
+ - a
+ - e
+
+Style/SingleLineMethods:
+ AllowIfMethodIsEmpty: true
+
+Layout/SpaceBeforeFirstArg:
+ # When true, allows most uses of extra spacing if the intent is to align
+ # things with the previous or next line, not counting empty lines or comment
+ # lines.
+ AllowForAlignment: true
+
+Style/SpecialGlobalVars:
+ EnforcedStyle: use_english_names
+ SupportedStyles:
+ - use_perl_names
+ - use_english_names
+
+Style/StabbyLambdaParentheses:
+ EnforcedStyle: require_parentheses
+ SupportedStyles:
+ - require_parentheses
+ - require_no_parentheses
+
+Style/StringLiterals:
+ EnforcedStyle: double_quotes
+ SupportedStyles:
+ - single_quotes
+ - double_quotes
+ # If true, strings which span multiple lines using \ for continuation must
+ # use the same type of quotes on each line.
+ ConsistentQuotesInMultiline: false
+
+Style/StringLiteralsInInterpolation:
+ EnforcedStyle: double_quotes
+ SupportedStyles:
+ - single_quotes
+ - double_quotes
+
+Style/StringMethods:
+ # Mapping from undesired method to desired_method
+ # e.g. to use `to_sym` over `intern`:
+ #
+ # StringMethods:
+ # PreferredMethods:
+ # intern: to_sym
+ PreferredMethods:
+ intern: to_sym
+
+Layout/SpaceAroundBlockParameters:
+ EnforcedStyleInsidePipes: no_space
+
+Layout/SpaceAroundEqualsInParameterDefault:
+ EnforcedStyle: space
+ SupportedStyles:
+ - space
+ - no_space
+
+Layout/SpaceAroundOperators:
+ # When true, allows most uses of extra spacing if the intent is to align
+ # with an operator on the previous or next line, not counting empty lines
+ # or comment lines.
+ AllowForAlignment: true
+
+Layout/SpaceBeforeBlockBraces:
+ EnforcedStyle: space
+ SupportedStyles:
+ - space
+ - no_space
+
+Layout/SpaceInsideBlockBraces:
+ EnforcedStyle: space
+ SupportedStyles:
+ - space
+ - no_space
+ # Valid values are: space, no_space
+ EnforcedStyleForEmptyBraces: no_space
+ # Space between { and |. Overrides EnforcedStyle if there is a conflict.
+ SpaceBeforeBlockParameters: true
+
+Layout/SpaceInsideHashLiteralBraces:
+ EnforcedStyle: space
+ EnforcedStyleForEmptyBraces: no_space
+ SupportedStyles:
+ - space
+ - no_space
+ # "compact" normally requires a space inside hash braces, with the exception
+ # that successive left braces or right braces are collapsed together
+ - compact
+
+Layout/SpaceInsideStringInterpolation:
+ EnforcedStyle: no_space
+ SupportedStyles:
+ - space
+ - no_space
+
+Style/AccessModifierDeclarations:
+ Enabled: false
+
+Style/SymbolArray:
+ EnforcedStyle: brackets
+ SupportedStyles:
+ - percent
+ - brackets
+
+Style/SymbolProc:
+ # A list of method names to be ignored by the check.
+ # The names should be fairly unique, otherwise you'll end up ignoring lots of code.
+ IgnoredMethods:
+ - respond_to
+ - define_method
+
+Style/TernaryParentheses:
+ EnforcedStyle: require_no_parentheses
+ SupportedStyles:
+ - require_parentheses
+ - require_no_parentheses
+ AllowSafeAssignment: true
+
+Layout/TrailingEmptyLines:
+ EnforcedStyle: final_newline
+ SupportedStyles:
+ - final_newline
+ - final_blank_line
+
+Style/TrailingCommaInArguments:
+ # If `comma`, the cop requires a comma after the last argument, but only for
+ # parenthesized method calls where each argument is on its own line.
+ # If `consistent_comma`, the cop requires a comma after the last argument,
+ # for all parenthesized method calls with arguments.
+ EnforcedStyleForMultiline: no_comma
+
+Style/TrailingCommaInArrayLiteral:
+ # If `comma`, the cop requires a comma after the last item in an array or
+ # hash, but only when each item is on its own line.
+ # If `consistent_comma`, the cop requires a comma after the last item of all
+ # non-empty array and hash literals.
+ EnforcedStyleForMultiline: no_comma
+
+Style/TrailingCommaInHashLiteral:
+ # If `comma`, the cop requires a comma after the last item in an array or
+ # hash, but only when each item is on its own line.
+ # If `consistent_comma`, the cop requires a comma after the last item of all
+ # non-empty array and hash literals.
+ EnforcedStyleForMultiline: no_comma
+
+# TrivialAccessors requires exact name matches and doesn't allow
+# predicated methods by default.
+Style/TrivialAccessors:
+ # When set to false the cop will suggest the use of accessor methods
+ # in situations like:
+ #
+ # def name
+ # @other_name
+ # end
+ #
+ # This way you can uncover "hidden" attributes in your code.
+ ExactNameMatch: true
+ AllowPredicates: true
+ # Allows trivial writers that don't end in an equal sign. e.g.
+ #
+ # def on_exception(action)
+ # @on_exception=action
+ # end
+ # on_exception :restart
+ #
+ # Commonly used in DSLs
+ AllowDSLWriters: false
+ IgnoreClassMethods: false
+ AllowedMethods:
+ - to_ary
+ - to_a
+ - to_c
+ - to_enum
+ - to_h
+ - to_hash
+ - to_i
+ - to_int
+ - to_io
+ - to_open
+ - to_path
+ - to_proc
+ - to_r
+ - to_regexp
+ - to_str
+ - to_s
+ - to_sym
+
+Naming/VariableName:
+ EnforcedStyle: snake_case
+ SupportedStyles:
+ - snake_case
+ - camelCase
+
+Naming/VariableNumber:
+ EnforcedStyle: normalcase
+ SupportedStyles:
+ - snake_case
+ - normalcase
+ - non_integer
+
+# WordArray enforces how array literals of word-like strings should be expressed.
+Style/WordArray:
+ EnforcedStyle: percent
+ SupportedStyles:
+ # percent style: %w(word1 word2)
+ - percent
+ # bracket style: ["word1", "word2"]
+ - brackets
+ # The MinSize option causes the WordArray rule to be ignored for arrays
+ # smaller than a certain size. The rule is only applied to arrays
+ # whose element count is greater than or equal to MinSize.
+ MinSize: 2
+ # The regular expression WordRegex decides what is considered a word.
+ WordRegex: !ruby/regexp '/\A[\p{Word}\n\t]+\z/'
+
+##################### Metrics ##################################
+
+Metrics/AbcSize:
+ # The ABC size is a calculated magnitude, so this number can be an Integer or
+ # a Float.
+ Max: 15
+ Enabled: false
+
+Metrics/BlockNesting:
+ Max: 3
+
+Metrics/ClassLength:
+ CountComments: false # count full line comments?
+ Max: 100
+ Enabled: false
+
+Metrics/ModuleLength:
+ CountComments: false # count full line comments?
+ Max: 100
+ Enabled: false
+
+# Avoid complex methods.
+Metrics/CyclomaticComplexity:
+ Max: 9
+ Exclude:
+ - "**/*/permissions.rb"
+
+Layout/LineLength:
+ Max: 180
+ # To make it possible to copy or click on URIs in the code, we allow lines
+ # containing a URI to be longer than Max.
+ AllowHeredoc: true
+ AllowURI: true
+ URISchemes:
+ - http
+ - https
+ Exclude:
+ - "**/spec/**/*"
+
+Metrics/MethodLength:
+ CountComments: false # count full line comments?
+ Max: 15
+ Enabled: false
+
+Metrics/ParameterLists:
+ Max: 5
+ CountKeywordArgs: true
+ Exclude:
+ - "decidim-core/lib/decidim/filter_form_builder.rb"
+
+Metrics/PerceivedComplexity:
+ Max: 10
+ Exclude:
+ - "**/*/permissions.rb"
+
+##################### Lint ##################################
+
+Lint/AmbiguousBlockAssociation:
+ Enabled: true
+ Exclude:
+ - "**/abilities/**/*"
+
+# Allow safe assignment in conditions.
+Lint/AssignmentInCondition:
+ AllowSafeAssignment: true
+
+# checks whether the end keywords are aligned properly for `do` `end` blocks.
+Layout/BlockAlignment:
+ # The value `start_of_block` means that the `end` should be aligned with line
+ # where the `do` keyword appears.
+ # The value `start_of_line` means it should be aligned with the whole
+ # expression's starting line.
+ # The value `either` means both are allowed.
+ EnforcedStyleAlignWith: either
+
+# Align ends correctly.
+Layout/EndAlignment:
+ # The value `keyword` means that `end` should be aligned with the matching
+ # keyword (if, while, etc.).
+ # The value `variable` means that in assignments, `end` should be aligned
+ # with the start of the variable on the left hand side of `=`. In all other
+ # situations, `end` should still be aligned with the keyword.
+ # The value `start_of_line` means that `end` should be aligned with the start
+ # of the line which the matching keyword appears on.
+ EnforcedStyleAlignWith: keyword
+ AutoCorrect: false
+
+Layout/DefEndAlignment:
+ # The value `def` means that `end` should be aligned with the def keyword.
+ # The value `start_of_line` means that `end` should be aligned with method
+ # calls like `private`, `public`, etc, if present in front of the `def`
+ # keyword on the same line.
+ EnforcedStyleAlignWith: start_of_line
+ AutoCorrect: false
+
+Lint/InheritException:
+ # The default base class in favour of `Exception`.
+ EnforcedStyle: runtime_error
+ SupportedStyles:
+ - runtime_error
+ - standard_error
+
+# Checks for unused block arguments
+Lint/UnusedBlockArgument:
+ IgnoreEmptyBlocks: true
+ AllowUnusedKeywordArguments: false
+
+# Checks for unused method arguments.
+Lint/UnusedMethodArgument:
+ AllowUnusedKeywordArguments: false
+ IgnoreEmptyMethods: true
+
+##################### Performance ############################
+
+Metrics/BlockLength:
+ Enabled: false
+
+RSpec/BeforeAfterAll:
+ Enabled: true
+
+RSpec/ContextWording:
+ Enabled: true
+ Prefixes:
+ - when
+ - with
+ - without
+ - and
+
+RSpec/DescribeClass:
+ Exclude:
+ - spec/gemfiles_spec.rb
+ - spec/js_bundles_spec.rb
+ - spec/i18n_spec.rb
+ - "**/*/spec/**/*_badge_spec.rb"
+ - decidim-core/spec/lib/global_engines_spec.rb
+ - "**/tasks/**/*"
+
+RSpec/EmptyExampleGroup:
+ Exclude:
+ - decidim-core/spec/lib/participatory_space_manifest_spec.rb
+
+RSpec/ExampleLength:
+ Max: 49
+
+RSpec/ExpectInHook:
+ Enabled: false
+
+RSpec/IteratedExpectation:
+ Enabled: true
+
+RSpec/LetSetup:
+ Enabled: false
+
+RSpec/MessageSpies:
+ Enabled: false
+
+RSpec/MultipleExpectations:
+ Enabled: false
+
+RSpec/NestedGroups:
+ Max: 7
+
+RSpec/NamedSubject:
+ Enabled: false
+
+RSpec/RepeatedExampleGroupDescription:
+ Enabled: false
+
+RSpec/RepeatedExampleGroupBody:
+ Enabled: false
+RSpec/VerifiedDoubles:
+ Enabled: false
+
+RSpec/LeakyConstantDeclaration:
+ Enabled: false
+
+RSpec/DescribedClass:
+ Enabled: false
+
+RSpec/MultipleMemoizedHelpers:
+ Max: 15
+
+inherit_from: .rubocop_rails.yml
diff --git a/.rubocop_rails.yml b/.rubocop_rails.yml
new file mode 100644
index 0000000000..399e265df6
--- /dev/null
+++ b/.rubocop_rails.yml
@@ -0,0 +1,83 @@
+require: rubocop-rails
+
+Rails:
+ Enabled: true
+
+Rails/ActionFilter:
+ Include:
+ - app/controllers/**/*.rb
+
+Rails/CreateTableWithTimestamps:
+ Enabled: false
+
+Rails/EnumUniqueness:
+ Include:
+ - app/models/**/*.rb
+
+Rails/Exit:
+ Include:
+ - app/**/*.rb
+ - config/**/*.rb
+ - lib/**/*.rb
+ Exclude:
+ - lib/**/*.rake
+
+Rails/FindBy:
+ Include:
+ - "**/*.rb"
+
+Rails/FindEach:
+ Include:
+ - app/models/**/*.rb
+
+Rails/HasAndBelongsToMany:
+ Include:
+ - app/models/**/*.rb
+
+Rails/HasManyOrHasOneDependent:
+ Include:
+ - app/models/**/*.rb
+
+Rails/InverseOf:
+ Enabled: false
+
+Rails/LexicallyScopedActionFilter:
+ Include:
+ - app/controllers/**/*.rb
+
+Rails/NotNullColumn:
+ Enabled: false
+
+Rails/Output:
+ Include:
+ - app/**/*.rb
+ - config/**/*.rb
+ - db/**/*.rb
+ - lib/**/*.rb
+ Exclude:
+ - db/seeds.rb
+ - lib/decidim/core.rb
+ - lib/decidim/component_manifest.rb
+ - lib/decidim/participatory_space_manifest.rb
+ - db/seeds.rb
+
+Rails/OutputSafety:
+ Enabled: false
+
+Rails/ReadWriteAttribute:
+ Include:
+ - app/models/**/*.rb
+
+Rails/ReversibleMigration:
+ Enabled: false
+
+Rails/ScopeArgs:
+ Include:
+ - app/models/**/*.rb
+
+Rails/SkipsModelValidations:
+ Enabled: true
+
+Rails/Validation:
+ Include:
+ - app/models/**/*.rb
diff --git a/Gemfile b/Gemfile
index ba3365cdd7..d6623b4599 100644
--- a/Gemfile
+++ b/Gemfile
@@ -56,15 +56,15 @@ end
group :production do
# gem "rubocop-rails"
- gem "passenger"
- gem "fog-aws"
gem "dalli"
- gem "sendgrid-ruby"
- gem "newrelic_rpm"
+ gem "fog-aws"
gem "lograge"
- gem "sidekiq"
- gem "sidekiq-scheduler"
- gem "sentry-ruby"
+ gem "newrelic_rpm"
+ gem "passenger"
+ gem "sendgrid-ruby"
gem "sentry-rails"
+ gem "sentry-ruby"
gem "sentry-sidekiq"
+ gem "sidekiq"
+ gem "sidekiq-scheduler"
end
diff --git a/Rakefile b/Rakefile
index e85f913914..d2a78aa258 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-require_relative 'config/application'
+require_relative "config/application"
Rails.application.load_tasks
diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb
index d672697283..9aec230539 100644
--- a/app/channels/application_cable/channel.rb
+++ b/app/channels/application_cable/channel.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb
index 0ff5442f47..8d6c2a1bf4 100644
--- a/app/channels/application_cable/connection.rb
+++ b/app/channels/application_cable/connection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 1c07694e9d..280cc28ce2 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
diff --git a/app/controllers/decidim_controller.rb b/app/controllers/decidim_controller.rb
index d61ffd097d..85919c078e 100644
--- a/app/controllers/decidim_controller.rb
+++ b/app/controllers/decidim_controller.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Entry point for Decidim. It will use the `DecidimController` as
# entry point, but you can change what controller it inherits from
# so you can customize some methods.
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index de6be7945c..15b06f0f67 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
module ApplicationHelper
end
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
index a009ace51c..d92ffddcb5 100644
--- a/app/jobs/application_job.rb
+++ b/app/jobs/application_job.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
class ApplicationJob < ActiveJob::Base
end
diff --git a/app/jobs/calculate_all_metrics_job.rb b/app/jobs/calculate_all_metrics_job.rb
index 8032135cf3..445f90eb8b 100644
--- a/app/jobs/calculate_all_metrics_job.rb
+++ b/app/jobs/calculate_all_metrics_job.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
require "rake"
class CalculateAllMetricsJob < ApplicationJob
diff --git a/app/jobs/orders_reminder_job.rb b/app/jobs/orders_reminder_job.rb
index 4e3cb6a11c..de83079f5a 100644
--- a/app/jobs/orders_reminder_job.rb
+++ b/app/jobs/orders_reminder_job.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "rake"
class OrdersReminderJob < ApplicationJob
diff --git a/app/jobs/preload_open_data_job.rb b/app/jobs/preload_open_data_job.rb
index 7ba482e522..2174a08732 100644
--- a/app/jobs/preload_open_data_job.rb
+++ b/app/jobs/preload_open_data_job.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
require "rake"
class PreloadOpenDataJob < ApplicationJob
diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb
index 3f44e0c669..04b52fc842 100644
--- a/app/mailers/application_mailer.rb
+++ b/app/mailers/application_mailer.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
class ApplicationMailer < Decidim::ApplicationMailer
- default from: 'from@example.com'
- layout 'mailer'
+ default from: "from@example.com"
+ layout "mailer"
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 10a4cba84d..71fbba5b32 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
diff --git a/app/services/decidim/osp_authorization_handler.rb b/app/services/decidim/osp_authorization_handler.rb
index 92a50413fd..4ae9a77ca3 100644
--- a/app/services/decidim/osp_authorization_handler.rb
+++ b/app/services/decidim/osp_authorization_handler.rb
@@ -17,8 +17,6 @@ def unique_id
document_number
end
- private
-
# An example implementation of a DefaultActionAuthorizer inherited class to override authorization status
# checking process. In this case, it allows to set a list of valid postal codes for an authorization.
# class ActionAuthorizer < Decidim::Verifications::DefaultActionAuthorizer
diff --git a/app/views/layouts/decidim/_main_footer.html.erb b/app/views/layouts/decidim/_main_footer.html.erb
index e22ea12404..d99e5cb1d6 100644
--- a/app/views/layouts/decidim/_main_footer.html.erb
+++ b/app/views/layouts/decidim/_main_footer.html.erb
@@ -2,25 +2,34 @@
-
- <% if current_organization.static_pages.any? %>
- <% current_organization.static_page_topics.where(show_in_footer: true).each do |page_topic| %>
- <% if page_topic.pages.any? %>
-
-
+
<%= display_flash_messages %>
<%= yield %>
diff --git a/config.ru b/config.ru
index f7ba0b527b..bff88d608a 100644
--- a/config.ru
+++ b/config.ru
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
# This file is used by Rack-based servers to start the application.
-require_relative 'config/environment'
+require_relative "config/environment"
run Rails.application
diff --git a/config/application.rb b/config/application.rb
index 29508cf9a8..3ec66831e2 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -1,6 +1,8 @@
-require_relative 'boot'
+# frozen_string_literal: true
-require 'rails/all'
+require_relative "boot"
+
+require "rails/all"
# TODO : add missing dep to decidim-initiatives/lib/decidim/initiatives/engine.rb
# require "wicked_pdf"
@@ -14,7 +16,7 @@ class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.2
config.time_zone = "Europe/Paris"
- config.i18n.load_path += Dir[Rails.root.join('config/locales/**/*.yml').to_s]
+ config.i18n.load_path += Dir[Rails.root.join("config/locales/**/*.yml").to_s]
# This needs to be set for correct images URLs in emails
# DON'T FORGET to ALSO set this in `config/initializers/carrierwave.rb`
diff --git a/config/boot.rb b/config/boot.rb
index b9e460cef3..aef6d031ee 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,4 +1,6 @@
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+# frozen_string_literal: true
-require 'bundler/setup' # Set up gems listed in the Gemfile.
-require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
+
+require "bundler/setup" # Set up gems listed in the Gemfile.
+require "bootsnap/setup" # Speed up boot time by caching expensive operations.
diff --git a/config/environment.rb b/config/environment.rb
index 426333bb46..7df99e89c6 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
# Load the Rails application.
-require_relative 'application'
+require_relative "application"
# Initialize the Rails application.
Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 4088495273..99b3b29eec 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -14,12 +16,12 @@
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
- if Rails.root.join('tmp', 'caching-dev.txt').exist?
+ if Rails.root.join("tmp/caching-dev.txt").exist?
config.action_controller.perform_caching = true
config.cache_store = :memory_store
config.public_file_server.headers = {
- 'Cache-Control' => "public, max-age=#{2.days.to_i}"
+ "Cache-Control" => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = false
@@ -34,7 +36,6 @@
# Store uploaded files on the local file system (see config/storage.yml for options)
config.active_storage.service = :local
-
config.action_mailer.perform_caching = false
# Print deprecation notices to the Rails logger.
diff --git a/config/environments/production.rb b/config/environments/production.rb
index a74bb7b361..200e16246d 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -11,7 +13,7 @@
config.eager_load = true
# Full error reports are disabled and caching is turned on.
- config.consider_all_requests_local = false
+ config.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
@@ -20,10 +22,10 @@
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
- config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
+ config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
# Compress JavaScripts and CSS.
- config.assets.js_compressor = Uglifier.new(:harmony => true)
+ config.assets.js_compressor = Uglifier.new(harmony: true)
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = true
@@ -53,16 +55,16 @@
config.log_level = :debug
# Prepend all log lines with the following tags.
- config.log_tags = [ :request_id ]
+ config.log_tags = [:request_id]
# Use a different cache store in production.
- if ENV["MEMCACHEDCLOUD_SERVERS"].present?
- config.cache_store = :dalli_store, ENV["MEMCACHEDCLOUD_SERVERS"].split(","), {
- username: ENV["MEMCACHEDCLOUD_USERNAME"], password: ENV["MEMCACHEDCLOUD_PASSWORD"]
- }
- else
- config.cache_store = :mem_cache_store
- end
+ config.cache_store = if ENV["MEMCACHEDCLOUD_SERVERS"].present?
+ [:dalli_store, ENV["MEMCACHEDCLOUD_SERVERS"].split(","), {
+ username: ENV["MEMCACHEDCLOUD_USERNAME"], password: ENV["MEMCACHEDCLOUD_PASSWORD"]
+ }]
+ else
+ :mem_cache_store
+ end
# Use a real queuing backend for Active Job (and separate queues per environment)
config.active_job.queue_adapter = :sidekiq
@@ -87,22 +89,22 @@
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
- :address => Rails.application.secrets.smtp_address,
- :port => Rails.application.secrets.smtp_port,
- :authentication => Rails.application.secrets.smtp_authentication,
- :user_name => Rails.application.secrets.smtp_username,
- :password => Rails.application.secrets.smtp_password,
- :domain => Rails.application.secrets.smtp_domain,
- :enable_starttls_auto => Rails.application.secrets.smtp_starttls_auto,
- :openssl_verify_mode => 'none'
+ address: Rails.application.secrets.smtp_address,
+ port: Rails.application.secrets.smtp_port,
+ authentication: Rails.application.secrets.smtp_authentication,
+ user_name: Rails.application.secrets.smtp_username,
+ password: Rails.application.secrets.smtp_password,
+ domain: Rails.application.secrets.smtp_domain,
+ enable_starttls_auto: Rails.application.secrets.smtp_starttls_auto,
+ openssl_verify_mode: "none"
}
if Rails.application.secrets.sendgrid
config.action_mailer.default_options = {
"X-SMTPAPI" => {
- filters: {
+ filters: {
clicktrack: { settings: { enable: 0 } },
- opentrack: { settings: { enable: 0 } }
+ opentrack: { settings: { enable: 0 } }
}
}.to_json
}
@@ -116,10 +118,10 @@
config.lograge.custom_options = lambda do |event|
{
remote_ip: event.payload[:remote_ip],
- params: event.payload[:params].except('controller', 'action', 'format', 'utf8'),
+ params: event.payload[:params].except("controller", "action", "format", "utf8"),
user_id: event.payload[:user_id],
organization_id: event.payload[:organization_id],
- referer: event.payload[:referer],
+ referer: event.payload[:referer]
}
end
@@ -128,9 +130,9 @@
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
if ENV["RAILS_LOG_TO_STDOUT"].present?
- logger = ActiveSupport::Logger.new(STDOUT)
+ logger = ActiveSupport::Logger.new($stdout)
logger.formatter = config.log_formatter
- config.logger = ActiveSupport::TaggedLogging.new(logger)
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Do not dump schema after migrations.
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 0a38fd3ce9..5169ddafb7 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
@@ -15,11 +17,11 @@
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
- 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
+ "Cache-Control" => "public, max-age=#{1.hour.to_i}"
}
# Show full error reports and disable caching.
- config.consider_all_requests_local = true
+ config.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Raise exceptions instead of rendering exception templates.
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
new file mode 100644
index 0000000000..277f407be3
--- /dev/null
+++ b/config/i18n-tasks.yml
@@ -0,0 +1,98 @@
+# i18n-tasks finds and manages missing and unused translations: https://github.com/glebm/i18n-tasks
+
+# The "main" locale.
+base_locale: en
+
+## Reporting locale, default: en. Available: en, ru.
+# internal_locale: en
+
+# Read and write translations.
+data:
+ ## Translations are read from the file system. Supported format: YAML, JSON.
+ ## Provide a custom adapter:
+ # adapter: I18n::Tasks::Data::FileSystem
+
+ # Locale files or `File.find` patterns where translations are read from:
+ read:
+ - config/locales/%{locale}.yml
+
+ # Locale files to write new keys to, based on a list of key pattern => file rules. Matched from top to bottom:
+ # `i18n-tasks normalize -p` will force move the keys according to these rules
+ write:
+ - config/locales/%{locale}.yml
+
+ ## Specify the router (see Readme for details). Valid values: conservative_router, pattern_router, or a custom class.
+ # router: convervative_router
+
+ yaml:
+ write:
+ # do not wrap lines at 80 characters
+ line_width: -1
+
+ ## Pretty-print JSON:
+ # json:
+ # write:
+ # indent: ' '
+ # space: ' '
+ # object_nl: "\n"
+ # array_nl: "\n"
+
+# Find translate calls
+search:
+ ## Paths or `File.find` patterns to search in:
+ paths:
+ - app
+ - lib
+
+ # Root directories for relative keys resolution.
+ relative_roots:
+ - app/controllers
+ - app/helpers
+ - app/mailers
+ - app/presenters
+ - app/services
+ - app/views
+ - app/cells
+
+ ## Files or `File.fnmatch` patterns to exclude from search. Some files are always excluded regardless of this setting:
+ ## %w(*.jpg *.png *.gif *.svg *.ico *.eot *.otf *.ttf *.woff *.woff2 *.pdf *.css *.sass *.scss *.less *.yml *.json)
+ exclude:
+ - "*.jpeg"
+ - "*.odt"
+ - "*.docx"
+
+ ## Alternatively, the only files or `File.fnmatch patterns` to search in `paths`:
+ ## If specified, this settings takes priority over `exclude`, but `exclude` still applies.
+ # only: ["*.rb", "*.html.slim"]
+
+ ## If `strict` is `false`, guess usages such as t("categories.#{category}.title"). The default is `true`.
+ strict: false
+
+ ## Multiple scanners can be used. Their results are merged.
+ ## The options specified above are passed down to each scanner. Per-scanner options can be specified as well.
+ ## See this example of a custom scanner: https://github.com/glebm/i18n-tasks/wiki/A-custom-scanner-example
+
+## Google Translate
+# translation:
+# # Get an API key and set billing info at https://code.google.com/apis/console to use Google Translate
+# api_key: "AbC-dEf5"
+
+translation:
+ deepl_api_key: <%= ENV["DEEPL_API_KEY"] %>
+
+# Do not consider these keys missing:
+ignore_missing:
+ - faker.*
+
+# Consider these keys used:
+ignore_unused:
+ - faker.*
+ - activemodel.attributes.osp_authorization_handler.*
+ - decidim.authorization_handlers.osp_authorization_handler.*
+ - decidim.authorization_handlers.osp_authorization_workflow.*
+ - decidim.verifications.authorizations.first_login.actions.osp_authorization_handler
+ - decidim.verifications.authorizations.first_login.actions.osp_authorization_workflow
+ - activemodel.attributes.participatory_process.private_space
+ - decidim.amendments.emendation.announcement.evaluating
+
+
diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb
index 89d2efab2b..f4556db399 100644
--- a/config/initializers/application_controller_renderer.rb
+++ b/config/initializers/application_controller_renderer.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# ActiveSupport::Reloader.to_prepare do
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index 4b828e80cb..ba194685a2 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -1,12 +1,14 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
-Rails.application.config.assets.version = '1.0'
+Rails.application.config.assets.version = "1.0"
# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
# Add Yarn node_modules folder to the asset load path.
-Rails.application.config.assets.paths << Rails.root.join('node_modules')
+Rails.application.config.assets.paths << Rails.root.join("node_modules")
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in the app/assets
diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb
index 59385cdf37..d0f0d3b5df 100644
--- a/config/initializers/backtrace_silencers.rb
+++ b/config/initializers/backtrace_silencers.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb
index f4c1f23518..fd7e11777f 100644
--- a/config/initializers/carrierwave.rb
+++ b/config/initializers/carrierwave.rb
@@ -13,24 +13,22 @@
end
# Setup CarrierWave to use Amazon S3. Add `gem "fog-aws" to your Gemfile.
-if ENV["HEROKU_APP_NAME"].present?
- if Rails.env.production?
- require 'carrierwave/storage/fog'
+if ENV["HEROKU_APP_NAME"].present? && Rails.env.production?
+ require "carrierwave/storage/fog"
- CarrierWave.configure do |config|
- config.storage = :fog
- config.fog_provider = 'fog/aws' # required
- config.fog_credentials = {
- provider: 'AWS', # required
- aws_access_key_id: Rails.application.secrets.aws_access_key_id, # required
- aws_secret_access_key: Rails.application.secrets.aws_secret_access_key, # required
- region: 'eu-central-1', # optional, defaults to 'us-east-1'
- host: 's3.eu-central-1.amazonaws.com', # optional, defaults to nil
- }
- config.fog_directory = 'decidim-heroku' # required
- config.fog_public = true # optional, defaults to true
- config.fog_attributes = { 'Cache-Control' => "max-age=#{365.day.to_i}" } # optional, defaults to {}
- config.storage = :fog
- end
+ CarrierWave.configure do |config|
+ config.storage = :fog
+ config.fog_provider = "fog/aws" # required
+ config.fog_credentials = {
+ provider: "AWS", # required
+ aws_access_key_id: Rails.application.secrets.aws_access_key_id, # required
+ aws_secret_access_key: Rails.application.secrets.aws_secret_access_key, # required
+ region: "eu-central-1", # optional, defaults to 'us-east-1'
+ host: "s3.eu-central-1.amazonaws.com" # optional, defaults to nil
+ }
+ config.fog_directory = "decidim-heroku" # required
+ config.fog_public = true # optional, defaults to true
+ config.fog_attributes = { "Cache-Control" => "max-age=#{365.days.to_i}" } # optional, defaults to {}
+ config.storage = :fog
end
end
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index d3bcaa5ec8..497f5667ce 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Define an application-wide content security policy
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
index 5a6a32d371..ee8dff9c99 100644
--- a/config/initializers/cookies_serializer.rb
+++ b/config/initializers/cookies_serializer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Specify a serializer for the signed and encrypted cookie jars.
diff --git a/config/initializers/decidim.rb b/config/initializers/decidim.rb
index 686c36962e..8e2db02430 100644
--- a/config/initializers/decidim.rb
+++ b/config/initializers/decidim.rb
@@ -12,10 +12,10 @@
config.maximum_attachment_height_or_width = 6000
# Geocoder configuration
- if !Rails.application.secrets.geocoder[:here_api_key].blank?
+ if Rails.application.secrets.geocoder[:here_api_key].present?
config.geocoder = {
- static_map_url: "https://image.maps.ls.hereapi.com/mia/1.6/mapview",
- here_api_key: Rails.application.secrets.geocoder[:here_api_key]
+ static_map_url: "https://image.maps.ls.hereapi.com/mia/1.6/mapview",
+ here_api_key: Rails.application.secrets.geocoder[:here_api_key]
}
end
@@ -27,7 +27,6 @@
Decidim::Initiatives.face_to_face_voting_allowed = false
end
-
# Custom resource reference generator method
# config.resource_reference_generator = lambda do |resource, feature|
# # Implement your custom method to generate resources references
@@ -84,17 +83,15 @@
# Decidim docs at docs/services/etherpad.md in order to set it up.
#
- if !Rails.application.secrets.etherpad[:server].blank?
+ if Rails.application.secrets.etherpad[:server].present?
config.etherpad = {
- server: Rails.application.secrets.etherpad[:server],
- api_key: Rails.application.secrets.etherpad[:api_key],
- api_version: Rails.application.secrets.etherpad[:api_version]
+ server: Rails.application.secrets.etherpad[:server],
+ api_key: Rails.application.secrets.etherpad[:api_key],
+ api_version: Rails.application.secrets.etherpad[:api_version]
}
end
- if ENV["HEROKU_APP_NAME"].present?
- config.base_uploads_path = ENV["HEROKU_APP_NAME"] + "/"
- end
+ config.base_uploads_path = "#{ENV["HEROKU_APP_NAME"]}/" if ENV["HEROKU_APP_NAME"].present?
end
Rails.application.config.i18n.available_locales = Decidim.available_locales
diff --git a/config/initializers/decidim-proposals.rb b/config/initializers/decidim_proposals.rb
similarity index 90%
rename from config/initializers/decidim-proposals.rb
rename to config/initializers/decidim_proposals.rb
index 3d732e8531..1b22dc2fd2 100644
--- a/config/initializers/decidim-proposals.rb
+++ b/config/initializers/decidim_proposals.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Decidim::Proposals.configure do |config|
# 0 means that 0% is required for the proposal to be proposed to the comparator.
# 100% means the proposals have to be fully similar.
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
index 4a994e1e7b..7a4f47b4c2 100644
--- a/config/initializers/filter_parameter_logging.rb
+++ b/config/initializers/filter_parameter_logging.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index ac033bf9dc..aa7435fbc9 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
index dc1899682b..6e1d16f027 100644
--- a/config/initializers/mime_types.rb
+++ b/config/initializers/mime_types.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
# Add new mime types for use in respond_to blocks:
diff --git a/config/initializers/new_framework_defaults_5_2.rb b/config/initializers/new_framework_defaults_5_2.rb
index c383d072bc..7df9ce8f45 100644
--- a/config/initializers/new_framework_defaults_5_2.rb
+++ b/config/initializers/new_framework_defaults_5_2.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
# Be sure to restart your server when you modify this file.
#
# This file contains migration options to ease your Rails 5.2 upgrade.
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index c2636c06b6..0d6adaa865 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "sentry-ruby"
if Rails.application.secrets.dig(:sentry, :enabled)
diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb
index bbfc3961bf..2f3c0db471 100644
--- a/config/initializers/wrap_parameters.rb
+++ b/config/initializers/wrap_parameters.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Be sure to restart your server when you modify this file.
# This file contains settings for ActionController::ParamsWrapper which
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 67e4a88d25..deee923192 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1,37 +1,57 @@
-# Files in the config/locales directory are used for internationalization
-# and are automatically loaded by Rails. If you want to use locales other
-# than English, add the necessary files in this directory.
-#
-# To use the locales, use `I18n.t`:
-#
-# I18n.t 'hello'
-#
-# In views, this is aliased to just `t`:
-#
-# <%= t('hello') %>
-#
-# To use a different locale, set it with `I18n.locale`:
-#
-# I18n.locale = :es
-#
-# This would use the information in config/locales/es.yml.
-#
-# The following keys must be escaped otherwise they will not be retrieved by
-# the default I18n backend:
-#
-# true, false, on, off, yes, no
-#
-# Instead, surround them with single quotes.
-#
-# en:
-# 'true': 'foo'
-#
-# To learn more, please read the Rails Internationalization guide
-# available at http://guides.rubyonrails.org/i18n.html.
-
+---
en:
+ activemodel:
+ attributes:
+ osp_authorization_handler:
+ birthday: Birthday
+ document_number: Unique number
+ postal_code: Postal code
+ participatory_process:
+ private_space: Private space
+ decidim:
+ accessibility:
+ skip_button: Skip button
+ admin:
+ participatory_space_private_users:
+ create:
+ error: Error
+ success: Success
+ amendments:
+ emendation:
+ announcement:
+ evaluating: |-
+ This amendment for %{amendable_type} %{proposal_link}
+ is in evaluation state.
+ authorization_handlers:
+ osp_authorization_handler:
+ explanation: Verify your identity by entering a unique number
+ fields:
+ birthday: Birthday
+ document_number: Unique number
+ postal_code: Postal code
+ name: Identity Verification Form
+ osp_authorization_workflow:
+ name: Authorization procedure
+ verifications:
+ authorizations:
+ first_login:
+ actions:
+ osp_authorization_handler: Verify with the identity verification form
+ osp_authorization_workflow: Verify with the identity verification form
+ faker:
+ address:
+ country_code: '["EN", "EN0", "EN1", "EN2", "EN3", "EN4", "EN5", "EN6", "EN7", "EN8", "EN9"]'
layouts:
decidim:
footer:
download_open_data: Open data
made_with_open_source: Website made by Open Source Politics with the decidim free software.
+ header:
+ close_menu: Close menu
+ navigation: Navigation
+ sign_in: Sign in
+ sign_up: Sign up
+ user_menu:
+ account: Account
+ conversations: Conversations
+ notifications: Notifications
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index a9796cb9ff..1973805a30 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -1,40 +1,68 @@
+---
fr:
activemodel:
attributes:
- participatory_process:
- private_space: Espace privé
osp_authorization_handler:
+ birthday: Date de naissance
document_number: Numéro unique
postal_code: Code postal
- birthday: Date de naissance
+ participatory_process:
+ private_space: Espace privé
decidim:
+ accessibility:
+ skip_button: Passer au contenu principal
+ admin:
+ participatory_space_private_users:
+ create:
+ error: Erreur
+ success: Succès
+ amendments:
+ emendation:
+ announcement:
+ evaluating: |-
+ Cet amendement pour le %{amendable_type} %{proposal_link}
+ est en cours d’évaluation.
authorization_handlers:
osp_authorization_handler:
explanation: Vérifier votre identité en saisissant un numéro unique
fields:
+ birthday: Date de naissance
document_number: Numéro unique
postal_code: Code postal
- birthday: Date de naissance
name: Formulaire de vérification d'identité
osp_authorization_workflow:
name: Procédure d'autorisation
- amendments:
- emendation:
- announcement:
- evaluating: |-
- Cet amendement pour le %{amendable_type} %{proposal_link}
- est en cours d’évaluation.
verifications:
authorizations:
first_login:
actions:
osp_authorization_handler: Vérifier avec le formulaire de vérification de l'identité
osp_authorization_workflow: Vérifier avec le formulaire de vérification de l'identité
+ faker:
+ address:
+ country_code:
+ - FR
+ - FR0
+ - FR1
+ - FR2
+ - FR3
+ - FR4
+ - FR5
+ - FR6
+ - FR7
+ - FR8
+ - FR9
layouts:
decidim:
footer:
download_open_data: Données ouvertes
made_with_open_source: Site réalisé par Open Source Politics grâce au logiciel libre Decidim.
- faker:
- address:
- country_code: ['FR','FR0','FR1','FR2','FR3','FR4','FR5','FR6','FR7','FR8','FR9']
+ header:
+ close_menu: Fermer
+ navigation: Navigation
+ sign_in: Se connecter
+ sign_up: S'inscrire
+ user_menu:
+ account: Mon compte
+ conversations: Conversations
+ notifications: Notifications
diff --git a/config/puma.rb b/config/puma.rb
index a5eccf816b..a8adec5d17 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -1,19 +1,21 @@
+# frozen_string_literal: true
+
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# 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_count = ENV.fetch("RAILS_MAX_THREADS", 5)
threads threads_count, threads_count
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
-port ENV.fetch("PORT") { 3000 }
+port ENV.fetch("PORT", 3000)
# Specifies the `environment` that Puma will run in.
#
-environment ENV.fetch("RAILS_ENV") { "development" }
+environment ENV.fetch("RAILS_ENV", "development")
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked webserver processes. If using threads and workers together
diff --git a/config/routes.rb b/config/routes.rb
index 8451871848..f7164ae2be 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,16 +1,16 @@
# frozen_string_literal: true
-require 'sidekiq/web'
-require 'sidekiq-scheduler/web'
+require "sidekiq/web"
+require "sidekiq-scheduler/web"
Rails.application.routes.draw do
authenticate :admin do
- mount Sidekiq::Web => '/sidekiq'
+ mount Sidekiq::Web => "/sidekiq"
end
- mount LetterOpenerWeb::Engine, at: '/letter_opener' if Rails.env.development?
+ mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
- mount Decidim::Core::Engine => '/'
+ mount Decidim::Core::Engine => "/"
# mount Decidim::Map::Engine => '/map'
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
diff --git a/config/spring.rb b/config/spring.rb
index 9fa7863f99..ff5ba06b6d 100644
--- a/config/spring.rb
+++ b/config/spring.rb
@@ -1,6 +1,8 @@
-%w[
+# frozen_string_literal: true
+
+%w(
.ruby-version
.rbenv-vars
tmp/restart.txt
tmp/caching-dev.txt
-].each { |path| Spring.watch(path) }
+).each { |path| Spring.watch(path) }
diff --git a/db/seeds.rb b/db/seeds.rb
index 212e65bde9..7eb3125d99 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
#
@@ -7,7 +9,7 @@
# Character.create(name: 'Luke', movie: movies.first)
# You can remove the 'faker' gem if you don't want Decidim seeds.
if ENV["HEROKU_APP_NAME"].present?
- ENV["DECIDIM_HOST"] = ENV["HEROKU_APP_NAME"] + ".herokuapp.com"
+ ENV["DECIDIM_HOST"] = "#{ENV["HEROKU_APP_NAME"]}.herokuapp.com"
ENV["SEED"] = "true"
end
Decidim.seed!
diff --git a/lib/extends/commands/decidim/admin/create_participatory_space_private_user_extends.rb b/lib/extends/commands/decidim/admin/create_participatory_space_private_user_extends.rb
index 51e58c1912..77130644cd 100644
--- a/lib/extends/commands/decidim/admin/create_participatory_space_private_user_extends.rb
+++ b/lib/extends/commands/decidim/admin/create_participatory_space_private_user_extends.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module CreateParticipatorySpacePrivateUserExtends
# Executes the command. Broadcasts these events:
#
diff --git a/lib/extends/commands/decidim/admin/impersonate_user_extends.rb b/lib/extends/commands/decidim/admin/impersonate_user_extends.rb
index 71485cc95e..381b335237 100644
--- a/lib/extends/commands/decidim/admin/impersonate_user_extends.rb
+++ b/lib/extends/commands/decidim/admin/impersonate_user_extends.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ImpersonateUserExtends
# Executes the command. Broadcasts these events:
#
diff --git a/lib/tasks/db.rake b/lib/tasks/db.rake
index fda00c71ce..9c32387ca9 100644
--- a/lib/tasks/db.rake
+++ b/lib/tasks/db.rake
@@ -1,14 +1,13 @@
# frozen_string_literal: true
namespace :decidim do
- Rails.logger = Logger.new(STDOUT)
- ActiveRecord::Base.logger = Logger.new(STDOUT)
-
namespace :db do
-
namespace :notification do
desc "List notifications related to orphans data"
task orphans: :environment do
+ Rails.logger = Logger.new($stdout)
+ ActiveRecord::Base.logger = Logger.new($stdout)
+
Decidim::Notification.distinct.pluck(:decidim_resource_type).each do |klass|
puts klass
model = klass.constantize
@@ -17,11 +16,15 @@ namespace :decidim do
.where.not(decidim_resource_id: [model.ids])
.pluck(:event_name, :decidim_resource_id, :extra).count
end
+
Rails.logger.close
end
desc "Delete notifications related to orphans data"
task clean: :environment do
+ Rails.logger = Logger.new($stdout)
+ ActiveRecord::Base.logger = Logger.new($stdout)
+
Decidim::Notification.distinct.pluck(:decidim_resource_type).each do |klass|
model = klass.constantize
Decidim::Notification
@@ -35,6 +38,9 @@ namespace :decidim do
namespace :admin_log do
desc "List admin log related to orphans data"
task orphans: :environment do
+ Rails.logger = Logger.new($stdout)
+ ActiveRecord::Base.logger = Logger.new($stdout)
+
Decidim::ActionLog.distinct.pluck(:resource_type).each do |klass|
puts klass
model = klass.constantize
@@ -48,6 +54,9 @@ namespace :decidim do
desc "Delete admin log related to orphans data"
task clean: :environment do
+ Rails.logger = Logger.new($stdout)
+ ActiveRecord::Base.logger = Logger.new($stdout)
+
Decidim::ActionLog.distinct.pluck(:resource_type).each do |klass|
model = klass.constantize
Decidim::ActionLog
@@ -61,6 +70,9 @@ namespace :decidim do
namespace :surveys do
desc "List surveys related to deleted component"
task orphans: :environment do
+ Rails.logger = Logger.new($stdout)
+ ActiveRecord::Base.logger = Logger.new($stdout)
+
Decidim::Surveys::Survey
.where.not(decidim_component_id: [Decidim::Component.ids])
.pluck(:id, :title, :decidim_component_id).each do |s|
@@ -71,6 +83,9 @@ namespace :decidim do
desc "Delete surveys related to deleted component"
task clean: :environment do
+ Rails.logger = Logger.new($stdout)
+ ActiveRecord::Base.logger = Logger.new($stdout)
+
Decidim::Surveys::Survey
.where.not(decidim_component_id: [Decidim::Component.ids])
.destroy_all
@@ -78,7 +93,5 @@ namespace :decidim do
Rails.logger.close
end
end
-
end
-
end
diff --git a/lib/tasks/heroku.rake b/lib/tasks/heroku.rake
index 68609fe77d..650943a196 100644
--- a/lib/tasks/heroku.rake
+++ b/lib/tasks/heroku.rake
@@ -1,28 +1,28 @@
# frozen_string_literal: true
namespace :heroku do
- desc 'Deploy a test version on heroku'
+ desc "Deploy a test version on heroku"
task setup: :environment do
- if ENV['SECRET_KEY_BASE'].nil?
- puts 'No SECRET_KEY_BASE found !'
- puts 'export SECRET_KEY_BASE first : '
+ if ENV["SECRET_KEY_BASE"].nil?
+ puts "No SECRET_KEY_BASE found !"
+ puts "export SECRET_KEY_BASE first : "
puts "export SECRET_KEY_BASE=#{`bundle exe rake secret`}"
exit 1
end
- if ENV['AWS_ACCESS_KEY_ID'].nil?
- puts 'No AWS_ACCESS_KEY_ID found !'
- puts 'export AWS_ACCESS_KEY_ID first'
+ if ENV["AWS_ACCESS_KEY_ID"].nil?
+ puts "No AWS_ACCESS_KEY_ID found !"
+ puts "export AWS_ACCESS_KEY_ID first"
exit 1
end
- if ENV['AWS_SECRET_ACCESS_KEY'].nil?
- puts 'No SECRET_KEY_BASE found !'
- puts 'export AWS_SECRET_ACCESS_KEY first'
+ if ENV["AWS_SECRET_ACCESS_KEY"].nil?
+ puts "No SECRET_KEY_BASE found !"
+ puts "export AWS_SECRET_ACCESS_KEY first"
exit 1
end
- skip_first_login_authorization = ENV['SKIP_FIRST_LOGIN_AUTHORIZATION'].nil? ? true : ENV['SKIP_FIRST_LOGIN_AUTHORIZATION']
+ skip_first_login_authorization = ENV["SKIP_FIRST_LOGIN_AUTHORIZATION"].nil? ? true : ENV["SKIP_FIRST_LOGIN_AUTHORIZATION"]
if system("heroku create #{app_name} --region eu")
system("heroku addons:create newrelic:wayne -a #{app_name}")
@@ -34,20 +34,19 @@ namespace :heroku do
system("heroku config:set SEED=true -a #{app_name}")
system("heroku config:set SKIP_FIRST_LOGIN_AUTHORIZATION=#{skip_first_login_authorization} -a #{app_name}")
system("heroku config:set SECRET_KEY_BASE=$SECRET_KEY_BASE -a #{app_name}")
- system('heroku config:set AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID')
- system('heroku config:set AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY')
+ system("heroku config:set AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID")
+ system("heroku config:set AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY")
system("heroku git:remote -a #{app_name}")
- if system("git push heroku $(git rev-parse --abbrev-ref HEAD):master")
- if system("heroku run rails db:migrate")
- system("heroku run rails db:seed")
- display_url
- end
+
+ if system("git push heroku $(git rev-parse --abbrev-ref HEAD):master") && system("heroku run rails db:migrate")
+ system("heroku run rails db:seed")
+ display_url
end
end
end
task push: :environment do
- system('git push heroku $(git rev-parse --abbrev-ref HEAD):master')
+ system("git push heroku $(git rev-parse --abbrev-ref HEAD):master")
system("heroku run rails db:migrate")
display_url
end
@@ -69,8 +68,8 @@ namespace :heroku do
app_name_raw = `git rev-parse --abbrev-ref HEAD`
digit = /\d.\d.-/.match(app_name_raw)
- return app_name_raw.tr('_', '-')[0..29].chomp if digit.nil?
+ return app_name_raw.tr("_", "-")[0..29].chomp if digit.nil?
- app_name_raw.gsub(digit[0], '').tr('_', '-')[0..29].chomp
+ app_name_raw.gsub(digit[0], "").tr("_", "-")[0..29].chomp
end
end
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 0da55ea0a2..df99f66223 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -1,232 +1,225 @@
# frozen_string_literal: true
-require 'ruby-progressbar'
+require "ruby-progressbar"
namespace :import do
- desc 'Usage: rake import:user FILE=\'\' ORG= ADMIN= PROCESS= [VERBOSE=true]\''
+ desc "Usage: rake import:user FILE='' ORG= ADMIN= PROCESS= [VERBOSE=true]'"
task user: :environment do
- Rails.application.config.active_job.queue_adapter = :inline
-
- @verbose = ENV['VERBOSE'].to_s == "true"
- if @verbose
- Rails.logger = Logger.new(STDOUT)
- else
- Rails.logger = Logger.new("log/import-user-#{Time.now.strftime '%Y-%m-%d-%H:%M:%S'}.log")
+ def validate_input
+ validate_file
+ validate_process
+ validate_admin
+ validate_org
end
- display_help unless ENV['FILE'] && ENV['ORG'] && ENV['ADMIN'] && ENV['PROCESS']
- @file = ENV['FILE']
- @org = ENV['ORG'].to_i
- @admin = ENV['ADMIN'].to_i
- @process = ENV['PROCESS'].to_i
- @auth_handler = ENV['AUTH_HANDLER']
-
- validate_input
-
- csv = CSV.read(@file, col_sep: ',', headers: true, skip_blanks: true)
- check_csv(csv)
-
- count = CSV.read(@file).count
-
- puts "CSV file is #{count} lines long"
-
- if !@verbose
- progressbar = ProgressBar.create(title: 'Importing User', total: count, format: '%t%e%B%p%%')
- end
+ def validate_org
+ if @org.class != Integer
+ puts "You must pass an organization id as an integer"
+ exit 1
+ end
- csv.each do |row|
- progressbar.increment unless @verbose
- # Import user with parsed informations id, first_name, last_name, email
- import_data(row[0], row[1], row[2], row[3])
+ unless current_organization
+ puts "Organization does not exist"
+ exit 1
+ end
end
- Rails.logger.close
- end
-end
-
-private
-
-def validate_input
- validate_file
- validate_process
- validate_admin
- validate_org
-end
+ def validate_admin
+ if @admin.class != Integer
+ puts "You must pass an admin id as an integer"
+ exit 1
+ end
-def validate_org
- if @org.class != Integer
- puts 'You must pass an organization id as an integer'
- exit 1
- end
+ unless current_user
+ puts "Admin does not exist"
+ exit 1
+ end
+ end
- unless current_organization
- puts 'Organization does not exist'
- exit 1
- end
-end
+ def validate_process
+ if @process.class != Integer
+ puts "You must pass a process id as an integer"
+ exit 1
+ end
-def validate_admin
- if @admin.class != Integer
- puts 'You must pass an admin id as an integer'
- exit 1
- end
+ unless current_process
+ puts "Process does not exist"
+ exit 1
+ end
+ end
- unless current_user
- puts 'Admin does not exist'
- exit 1
- end
-end
+ def validate_file
+ unless File.exist?(@file)
+ puts "File does not exist, be sure to pass a full path."
+ exit 1
+ end
-def validate_process
- if @process.class != Integer
- puts 'You must pass a process id as an integer'
- exit 1
- end
+ if File.extname(@file) != ".csv"
+ puts "You must pass a CSV file"
+ exit 1
+ end
+ end
- unless current_process
- puts 'Process does not exist'
- exit 1
- end
-end
+ def display_help
+ puts <<~HEREDOC
+ Help:
+ Usage: rake import:user FILE='' ORG= ADMIN= PROCESS=
+ HEREDOC
+ exit 0
+ end
-def validate_file
- unless File.exist?(@file)
- puts 'File does not exist, be sure to pass a full path.'
- exit 1
- end
+ def check_csv(file)
+ file.each do |row|
+ # Check if id, first_name, last_name are nil
+ next unless row[0].nil? || row[1].nil? || row[2].nil?
- if File.extname(@file) != '.csv'
- puts 'You must pass a CSV file'
- exit 1
- end
-end
+ puts "Something went wrong, empty field(s) on line #{$INPUT_LINE_NUMBER}"
+ puts row.inspect
+ exit 1
+ end
+ end
-def display_help
- puts <<~HEREDOC
- Help:
- Usage: rake import:user FILE='' ORG= ADMIN= PROCESS=
- HEREDOC
- exit 0
-end
+ def import_data(id, first_name, last_name, email)
+ # Extends are only loaded at the last time
+ require "extends/commands/decidim/admin/create_participatory_space_private_user_extends"
+ require "extends/commands/decidim/admin/impersonate_user_extends"
-def check_csv(file)
- file.each do |row|
- # Check if id, first_name, last_name are nil
- if row[0].nil? || row[1].nil? || row[2].nil?
- puts "Something went wrong, empty field(s) on line #{$INPUT_LINE_NUMBER}"
- puts row.inspect
- exit 1
+ if email.nil?
+ import_without_email(id, first_name, last_name)
+ else
+ import_with_email(id, first_name, last_name, email)
+ end
end
- end
-end
-def import_data(id, first_name, last_name, email)
+ def import_without_email(id, first_name, last_name)
+ new_user = Decidim::User.new(
+ managed: true,
+ name: set_name(first_name, last_name),
+ organization: current_organization,
+ admin: false,
+ roles: [],
+ tos_agreement: true
+ )
+ form = Decidim::Admin::ImpersonateUserForm.from_params(
+ user: new_user,
+ name: new_user.name,
+ reason: "import",
+ handler_name: "osp_authorization_handler",
+ authorization: Decidim::AuthorizationHandler.handler_for(
+ "osp_authorization_handler",
+ {
+ user: new_user,
+ document_number: id
+ }
+ )
+ ).with_context(
+ current_organization: current_organization,
+ current_user: current_user
+ )
- # Extends are only loaded at the last time
- require 'extends/commands/decidim/admin/create_participatory_space_private_user_extends.rb'
- require 'extends/commands/decidim/admin/impersonate_user_extends.rb'
+ privatable_to = current_process
- if email.nil?
- import_without_email(id, first_name, last_name)
- else
- import_with_email(id, first_name, last_name, email)
- end
-end
+ Decidim::Admin::ImpersonateUser.call(form) do
+ on(:ok) do |user|
+ Decidim::ParticipatorySpacePrivateUser.find_or_create_by!(
+ user: user,
+ privatable_to: privatable_to
+ )
+ Rails.logger.debug I18n.t("participatory_space_private_users.create.success", scope: "decidim.admin")
+ Rails.logger.debug "Registered user with id: #{id}, first_name: #{first_name}, last_name: #{last_name} --> #{user.id}"
+ end
+
+ on(:invalid) do
+ Rails.logger.debug I18n.t("participatory_space_private_users.create.error", scope: "decidim.admin")
+ Rails.logger.debug user.errors.full_messages if user.invalid?
+ Rails.logger.debug form.errors.full_messages if form.invalid?
+ Rails.logger.debug "Failed to register user with id: #{id}, first_name: #{first_name}, last_name: #{last_name} !!"
+ # exit 1
+ end
+ end
+ end
-def import_without_email(id, first_name, last_name)
- new_user = Decidim::User.new(
- managed: true,
- name: set_name(first_name, last_name),
- organization: current_organization,
- admin: false,
- roles: [],
- tos_agreement: true
- )
- form = Decidim::Admin::ImpersonateUserForm.from_params(
- user: new_user,
- name: new_user.name,
- reason: 'import',
- handler_name: 'osp_authorization_handler',
- authorization: Decidim::AuthorizationHandler.handler_for(
- 'osp_authorization_handler',
- {
- user: new_user,
- document_number: id
- }
- )
- ).with_context(
- current_organization: current_organization,
- current_user: current_user
- )
-
- privatable_to = current_process
-
- Decidim::Admin::ImpersonateUser.call(form) do
- on(:ok) do |user|
- Decidim::ParticipatorySpacePrivateUser.find_or_create_by!(
- user: user,
- privatable_to: privatable_to
+ def import_with_email(id, first_name, last_name, email)
+ form = Decidim::Admin::ParticipatorySpacePrivateUserForm.from_params(
+ {
+ name: set_name(first_name, last_name),
+ email: email
+ },
+ privatable_to: current_process
)
- Rails.logger.debug I18n.t("participatory_space_private_users.create.success", scope: "decidim.admin")
- Rails.logger.debug "Registered user with id: #{id}, first_name: #{first_name}, last_name: #{last_name} --> #{user.id}"
+ Decidim::Admin::CreateParticipatorySpacePrivateUser.call(form, current_user, current_process) do
+ on(:ok) do |user|
+ Decidim::Authorization.create_or_update_from(
+ Decidim::AuthorizationHandler.handler_for(
+ "osp_authorization_handler",
+ {
+ user: user,
+ document_number: id
+ }
+ )
+ )
+ Rails.logger.debug I18n.t("participatory_space_private_users.create.success", scope: "decidim.admin")
+ Rails.logger.debug "Registered user with id: #{id}, first_name: #{first_name}, last_name: #{last_name}, email: #{email} --> #{user.id}"
+ end
+
+ on(:invalid) do
+ Rails.logger.debug I18n.t("participatory_space_private_users.create.error", scope: "decidim.admin")
+ Rails.logger.debug form.errors.full_messages if form.invalid?
+ Rails.logger.debug "Failed to register user with id: #{id}, first_name: #{first_name}, last_name: #{last_name}, email: #{email} !!"
+ # exit 1
+ end
+ end
end
- on(:invalid) do
- Rails.logger.debug I18n.t("participatory_space_private_users.create.error", scope: "decidim.admin")
- Rails.logger.debug user.errors.full_messages if user.invalid?
- Rails.logger.debug form.errors.full_messages if form.invalid?
- Rails.logger.debug "Failed to register user with id: #{id}, first_name: #{first_name}, last_name: #{last_name} !!"
- # exit 1
+ def set_name(first_name, last_name)
+ "#{first_name} #{last_name}"
end
- end
-end
+ def current_user
+ @current_user ||= Decidim::User.find(@admin)
+ end
-def import_with_email(id, first_name, last_name, email)
- form = Decidim::Admin::ParticipatorySpacePrivateUserForm.from_params(
- {
- name: set_name(first_name, last_name),
- email: email
- },
- privatable_to: current_process
- )
- Decidim::Admin::CreateParticipatorySpacePrivateUser.call(form, current_user, current_process) do
- on(:ok) do |user|
- Decidim::Authorization.create_or_update_from(
- Decidim::AuthorizationHandler.handler_for(
- 'osp_authorization_handler',
- {
- user: user,
- document_number: id
- }
- )
- )
- Rails.logger.debug I18n.t("participatory_space_private_users.create.success", scope: "decidim.admin")
- Rails.logger.debug "Registered user with id: #{id}, first_name: #{first_name}, last_name: #{last_name}, email: #{email} --> #{user.id}"
+ def current_organization
+ @current_organization ||= Decidim::Organization.find(@org)
end
- on(:invalid) do
- Rails.logger.debug I18n.t("participatory_space_private_users.create.error", scope: "decidim.admin")
- Rails.logger.debug form.errors.full_messages if form.invalid?
- Rails.logger.debug "Failed to register user with id: #{id}, first_name: #{first_name}, last_name: #{last_name}, email: #{email} !!"
- # exit 1
+ def current_process
+ @current_process ||= Decidim::ParticipatoryProcess.find(@process)
end
- end
-end
+ Rails.application.config.active_job.queue_adapter = :inline
-def set_name(first_name, last_name)
- first_name + ' ' + last_name
-end
+ @verbose = ENV["VERBOSE"].to_s == "true"
+ Rails.logger = if @verbose
+ Logger.new($stdout)
+ else
+ Logger.new("log/import-user-#{Time.zone.now.strftime "%Y-%m-%d-%H:%M:%S"}.log")
+ end
-def current_user
- @current_user ||= Decidim::User.find(@admin)
-end
+ display_help unless ENV["FILE"] && ENV["ORG"] && ENV["ADMIN"] && ENV["PROCESS"]
+ @file = ENV["FILE"]
+ @org = ENV["ORG"].to_i
+ @admin = ENV["ADMIN"].to_i
+ @process = ENV["PROCESS"].to_i
+ @auth_handler = ENV["AUTH_HANDLER"]
-def current_organization
- @current_organization ||= Decidim::Organization.find(@org)
-end
+ validate_input
-def current_process
- @current_process ||= Decidim::ParticipatoryProcess.find(@process)
+ csv = CSV.read(@file, col_sep: ",", headers: true, skip_blanks: true)
+ check_csv(csv)
+
+ count = CSV.read(@file).count
+
+ puts "CSV file is #{count} lines long"
+
+ progressbar = ProgressBar.create(title: "Importing User", total: count, format: "%t%e%B%p%%") unless @verbose
+
+ csv.each do |row|
+ progressbar.increment unless @verbose
+ # Import user with parsed informations id, first_name, last_name, email
+ import_data(row[0], row[1], row[2], row[3])
+ end
+
+ Rails.logger.close
+ end
end
diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake
new file mode 100644
index 0000000000..69fb09e63e
--- /dev/null
+++ b/lib/tasks/test.rake
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+namespace :test do
+ desc "Setup tests environment"
+ task setup: :environment do
+ system("rake db:drop RAILS_ENV=test")
+ system("rake db:create RAILS_ENV=test")
+ system("rake db:migrate RAILS_ENV=test")
+ end
+end
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000000..1d04d2bd15
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,12 @@
+{
+ "name": "osp-app",
+ "version": "0.12.2",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "osp-app",
+ "version": "0.12.2"
+ }
+ }
+}
diff --git a/package.json b/package.json
index f5e0f91442..5687e3b383 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,5 @@
{
"name": "osp-app",
"version": "0.12.2",
- "private": true,
- "dependencies": {}
+ "private": true
}
diff --git a/spec/factories.rb b/spec/factories.rb
new file mode 100644
index 0000000000..e063e38680
--- /dev/null
+++ b/spec/factories.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require "decidim/core/test/factories"
+require "decidim/proposals/test/factories"
+require "decidim/debates/test/factories"
+require "decidim/meetings/test/factories"
+require "decidim/accountability/test/factories"
+require "decidim/system/test/factories"
+require "decidim/participatory_processes/test/factories"
diff --git a/spec/factory_bot_spec.rb b/spec/factory_bot_spec.rb
new file mode 100644
index 0000000000..d478a75b7f
--- /dev/null
+++ b/spec/factory_bot_spec.rb
@@ -0,0 +1,9 @@
+# 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/i18n_spec.rb b/spec/i18n_spec.rb
new file mode 100644
index 0000000000..1b1a151f6e
--- /dev/null
+++ b/spec/i18n_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "i18n/tasks"
+
+describe "I18n sanity" do
+ let(:locales) do
+ ENV["ENFORCED_LOCALES"].presence || "en,fr"
+ end
+
+ let(:i18n) { I18n::Tasks::BaseTask.new(locales: locales.split(",")) }
+ let(:missing_keys) { i18n.missing_keys }
+ let(:unused_keys) { i18n.unused_keys }
+ let(:non_normalized_paths) { i18n.non_normalized_paths }
+
+ it "does not have missing keys" do
+ expect(missing_keys).to be_empty, "#{missing_keys.inspect} are missing"
+ end
+
+ it "does not have unused keys" do
+ expect(unused_keys).to be_empty, "#{unused_keys.inspect} are unused"
+ end
+
+ unless ENV["SKIP_NORMALIZATION"]
+ it "is normalized" do
+ error_message = "The following files need to be normalized:\n" \
+ "#{non_normalized_paths.map { |path| " #{path}" }.join("\n")}\n" \
+ "Please run `bundle exec i18n-tasks normalize --locales #{locales}` to fix them"
+
+ expect(non_normalized_paths).to be_empty, error_message
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000000..fa62eb4d1b
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "decidim/dev"
+Decidim::Dev.dummy_app_path = File.expand_path(Rails.root.to_s)
+require "decidim/dev/test/base_spec_helper"
+
+DEFAULT_LOCALE = :en
+AVAILABLE_LOCALES = [:en, :ca, :es].freeze
+
+RSpec.configure do |config|
+ config.before do
+ # I18n configuration
+ I18n.available_locales = AVAILABLE_LOCALES
+ I18n.default_locale = DEFAULT_LOCALE
+ I18n.locale = DEFAULT_LOCALE
+
+ # Decidim configurations
+ Decidim.available_locales = AVAILABLE_LOCALES
+ Decidim.default_locale = DEFAULT_LOCALE
+
+ # Initializers configs
+ Decidim.enable_html_header_snippets = false
+ SocialShareButton.configure do |social_share_button|
+ social_share_button.allow_sites = %w(twitter facebook whatsapp_app whatsapp_web telegram)
+ end
+ end
+end
diff --git a/spec/system/decidim/account_spec.rb b/spec/system/decidim/account_spec.rb
new file mode 100644
index 0000000000..b018ddf4ca
--- /dev/null
+++ b/spec/system/decidim/account_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe "Account", type: :system do
+ let(:user) { create(:user, :confirmed, password: password, password_confirmation: password) }
+ let(:password) { "dqCFgjfDbC7dPbrv" }
+ let(:organization) { user.organization }
+
+ before do
+ switch_to_host(organization.host)
+ 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
+
+ describe "updating personal data" do
+ it "updates the user's data" do
+ within "form.edit_user" do
+ 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."
+ find("*[type=submit]").click
+ end
+
+ within_flash_messages do
+ expect(page).to have_content("successfully")
+ end
+
+ within ".title-bar" do
+ expect(page).to have_content("Nikola Tesla")
+ end
+
+ user.reload
+
+ within_user_menu do
+ find("a", text: "public profile").click
+ end
+
+ expect(page).to have_content("example.org")
+ expect(page).to have_content("Serbian-American")
+ end
+ end
+
+ describe "updating the password" do
+ context "when password and confirmation match" do
+ it "updates the password successfully" do
+ within "form.edit_user" do
+ page.find(".change-password").click
+
+ fill_in :user_password, with: "sekritpass123"
+ fill_in :user_password_confirmation, with: "sekritpass123"
+
+ find("*[type=submit]").click
+ end
+
+ within_flash_messages do
+ expect(page).to have_content("successfully")
+ end
+
+ expect(user.reload.valid_password?("sekritpass123")).to eq(true)
+ end
+ end
+
+ context "when passwords don't match" do
+ it "doesn't update the password" do
+ within "form.edit_user" do
+ page.find(".change-password").click
+
+ fill_in :user_password, with: "sekritpass123"
+ fill_in :user_password_confirmation, with: "oopseytypo"
+
+ find("*[type=submit]").click
+ end
+
+ within_flash_messages do
+ expect(page).to have_content("There was a problem")
+ end
+
+ expect(user.reload.valid_password?("sekritpass123")).to eq(false)
+ end
+ end
+
+ context "when updating the email" do
+ it "needs to confirm it" do
+ within "form.edit_user" do
+ fill_in :user_email, with: "foo@bar.com"
+
+ find("*[type=submit]").click
+ end
+
+ within_flash_messages do
+ expect(page).to have_content("email to confirm")
+ end
+ 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
+ within ".switch.newsletter_notifications" do
+ page.find(".switch-paddle").click
+ end
+
+ within "form.edit_user" do
+ find("*[type=submit]").click
+ end
+
+ within_flash_messages do
+ expect(page).to have_content("successfully")
+ end
+ end
+ end
+
+ context "when on the delete my account page" do
+ before do
+ visit decidim.delete_account_path
+ end
+
+ it "the user can delete his account" do
+ fill_in :delete_user_delete_account_delete_reason, with: "I just want to delete my account"
+
+ click_button "Delete my account"
+
+ click_button "Yes, I want to delete my account"
+
+ within_flash_messages do
+ expect(page).to have_content("successfully")
+ end
+
+ find(".sign-in-link").click
+
+ 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
+ end
+ end
+end
diff --git a/spec/system/decidim/authentication_spec.rb b/spec/system/decidim/authentication_spec.rb
new file mode 100644
index 0000000000..3f58aa4adf
--- /dev/null
+++ b/spec/system/decidim/authentication_spec.rb
@@ -0,0 +1,609 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe "Authentication", type: :system do
+ let(:organization) { create(:organization) }
+ let(:last_user) { Decidim::User.last }
+
+ before do
+ switch_to_host(organization.host)
+ visit decidim.root_path
+ end
+
+ describe "Sign Up" do
+ context "when using email and password" do
+ it "creates a new User" do
+ find(".sign-up-link").click
+
+ within ".new_user" do
+ fill_in :registration_user_email, with: "user@example.org"
+ fill_in :registration_user_name, with: "Responsible Citizen"
+ fill_in :registration_user_nickname, with: "responsible"
+ fill_in :registration_user_password, with: "DfyvHn425mYAy2HL"
+ fill_in :registration_user_password_confirmation, with: "DfyvHn425mYAy2HL"
+ check :registration_user_tos_agreement
+ check :registration_user_newsletter
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_content("You have signed up successfully")
+ end
+ end
+
+ context "when using another langage" do
+ before do
+ within_language_menu do
+ click_link "Castellano"
+ end
+ end
+
+ it "keeps the locale settings" do
+ find(".sign-up-link").click
+
+ within ".new_user" do
+ fill_in :registration_user_email, with: "user@example.org"
+ fill_in :registration_user_name, with: "Responsible Citizen"
+ fill_in :registration_user_nickname, with: "responsible"
+ fill_in :registration_user_password, with: "DfyvHn425mYAy2HL"
+ fill_in :registration_user_password_confirmation, with: "DfyvHn425mYAy2HL"
+ check :registration_user_tos_agreement
+ check :registration_user_newsletter
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_content("¡Bienvenida! Te has registrado con éxito.")
+ expect(last_user.locale).to eq("es")
+ end
+ end
+
+ context "when being a robot" do
+ it "denies the sign up" do
+ find(".sign-up-link").click
+
+ within ".new_user" do
+ page.execute_script("$($('.new_user > div > input')[0]).val('Ima robot :D')")
+ fill_in :registration_user_email, with: "user@example.org"
+ fill_in :registration_user_name, with: "Responsible Citizen"
+ fill_in :registration_user_nickname, with: "responsible"
+ fill_in :registration_user_password, with: "DfyvHn425mYAy2HL"
+ fill_in :registration_user_password_confirmation, with: "DfyvHn425mYAy2HL"
+ check :registration_user_tos_agreement
+ check :registration_user_newsletter
+ find("*[type=submit]").click
+ end
+
+ expect(page).not_to have_content("You have signed up successfully")
+ end
+ end
+
+ context "when using facebook" do
+ let(:omniauth_hash) do
+ OmniAuth::AuthHash.new(
+ provider: "facebook",
+ uid: "123545",
+ info: {
+ email: "user@from-facebook.com",
+ name: "Facebook User"
+ }
+ )
+ end
+
+ before do
+ OmniAuth.config.test_mode = true
+ OmniAuth.config.mock_auth[:facebook] = omniauth_hash
+ end
+
+ after do
+ OmniAuth.config.test_mode = false
+ OmniAuth.config.mock_auth[:facebook] = nil
+ end
+
+ context "when the user has confirmed the email in facebook" do
+ it "creates a new User without sending confirmation instructions" do
+ find(".sign-up-link").click
+
+ click_link "Sign in with Facebook"
+
+ expect(page).to have_content("Successfully")
+ expect_user_logged
+ end
+ end
+ end
+
+ context "when using twitter" do
+ let(:email) { nil }
+ let(:omniauth_hash) do
+ OmniAuth::AuthHash.new(
+ provider: "twitter",
+ uid: "123545",
+ info: {
+ name: "Twitter User",
+ nickname: "twitter_user",
+ email: email
+ }
+ )
+ end
+
+ before do
+ OmniAuth.config.test_mode = true
+ OmniAuth.config.mock_auth[:twitter] = omniauth_hash
+ end
+
+ after do
+ OmniAuth.config.test_mode = false
+ OmniAuth.config.mock_auth[:twitter] = nil
+ end
+
+ context "when the response doesn't include the email" do
+ it "redirects the user to a finish signup page" do
+ find(".sign-up-link").click
+
+ click_link "Sign in with Twitter"
+
+ expect(page).to have_content("Successfully")
+ expect(page).to have_content("Please complete your profile")
+
+ within ".new_user" do
+ fill_in :registration_user_email, with: "user@from-twitter.com"
+ find("*[type=submit]").click
+ end
+ end
+
+ context "and a user already exists with the given email" do
+ it "doesn't allow it" do
+ create(:user, :confirmed, email: "user@from-twitter.com", organization: organization)
+ find(".sign-up-link").click
+
+ click_link "Sign in with Twitter"
+
+ expect(page).to have_content("Successfully")
+ expect(page).to have_content("Please complete your profile")
+
+ within ".new_user" do
+ fill_in :registration_user_email, with: "user@from-twitter.com"
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_content("Please complete your profile")
+ expect(page).to have_content("Another account is using the same email address")
+ end
+ end
+ end
+
+ context "when the response includes the email" do
+ let(:email) { "user@from-twitter.com" }
+
+ it "creates a new User" do
+ find(".sign-up-link").click
+
+ click_link "Sign in with Twitter"
+
+ expect_user_logged
+ end
+ end
+ end
+
+ context "when using google" do
+ let(:omniauth_hash) do
+ OmniAuth::AuthHash.new(
+ provider: "google_oauth2",
+ uid: "123545",
+ info: {
+ name: "Google User",
+ email: "user@from-google.com"
+ }
+ )
+ end
+
+ before do
+ OmniAuth.config.test_mode = true
+ OmniAuth.config.mock_auth[:google_oauth2] = omniauth_hash
+ end
+
+ after do
+ OmniAuth.config.test_mode = false
+ OmniAuth.config.mock_auth[:google_oauth2] = nil
+ end
+
+ it "creates a new User" do
+ find(".sign-up-link").click
+
+ click_link "Sign in with Google"
+
+ expect_user_logged
+ end
+ end
+
+ context "when sign up is disabled" do
+ let(:organization) { create(:organization, users_registration_mode: :existing) }
+
+ it "redirects to the sign in when accessing the sign up page" do
+ visit decidim.new_user_registration_path
+ expect(page).not_to have_content("Sign Up")
+ end
+
+ it "don't allow the user to sign up" do
+ find(".sign-in-link").click
+ expect(page).not_to have_content("Create an account")
+ end
+ end
+ end
+
+ describe "Confirm email" do
+ it "confirms the user" do
+ perform_enqueued_jobs { create(:user, organization: organization) }
+
+ visit last_email_link
+
+ expect(page).to have_content("successfully confirmed")
+ expect(last_user).to be_confirmed
+ end
+ end
+
+ context "when confirming the account" do
+ let!(:user) { create(:user, email_on_notification: true, organization: organization) }
+
+ before do
+ perform_enqueued_jobs { user.confirm }
+ switch_to_host(user.organization.host)
+ login_as user, scope: :user
+ visit decidim.root_path
+ end
+
+ it "sends a welcome notification" do
+ find("a.topbar__notifications").click
+
+ within "#notifications" do
+ expect(page).to have_content("Welcome")
+ expect(page).to have_content("thanks for joining #{organization.name}")
+ end
+
+ expect(last_email_body).to include("thanks for joining #{organization.name}")
+ end
+ end
+
+ describe "Resend confirmation instructions" do
+ let(:user) do
+ perform_enqueued_jobs { create(:user, organization: organization) }
+ end
+
+ it "sends an email with the instructions" do
+ visit decidim.new_user_confirmation_path
+
+ within ".new_user" do
+ fill_in :confirmation_user_email, with: user.email
+ perform_enqueued_jobs { find("*[type=submit]").click }
+ end
+
+ expect(emails.count).to eq(2)
+ expect(page).to have_content("receive an email with instructions")
+ end
+ end
+
+ context "when a user is already registered" do
+ let(:user) { create(:user, :confirmed, password: "DfyvHn425mYAy2HL", organization: organization) }
+
+ describe "Sign in" do
+ it "authenticates an existing User" do
+ find(".sign-in-link").click
+
+ within ".new_user" do
+ fill_in :session_user_email, with: user.email
+ fill_in :session_user_password, with: "DfyvHn425mYAy2HL"
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_content("Signed in successfully")
+ expect(page).to have_content(user.name)
+ end
+
+ it "caches the omniauth buttons correctly with different languages", :caching do
+ find(".sign-in-link").click
+ expect(page).to have_content("Sign in with Facebook")
+
+ within_language_menu do
+ click_link "Català"
+ end
+
+ expect(page).to have_content("Inicia sessió amb Facebook")
+ end
+ end
+
+ describe "Forgot password" do
+ it "sends a password recovery email" do
+ visit decidim.new_user_password_path
+
+ within ".new_user" do
+ fill_in :password_user_email, with: user.email
+ perform_enqueued_jobs { find("*[type=submit]").click }
+ end
+
+ expect(page).to have_content("reset your password")
+ expect(emails.count).to eq(1)
+ end
+ end
+
+ describe "Reset password" do
+ before do
+ perform_enqueued_jobs { user.send_reset_password_instructions }
+ end
+
+ it "sets a new password for the user" do
+ visit last_email_link
+
+ within ".new_user" do
+ fill_in :password_user_password, with: "DfyvHn425mYAy2HL"
+ fill_in :password_user_password_confirmation, with: "DfyvHn425mYAy2HL"
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_content("Your password has been successfully changed")
+ expect(page).to have_current_path "/"
+ end
+ end
+
+ describe "Sign Out" do
+ before do
+ login_as user, scope: :user
+ visit decidim.root_path
+ end
+
+ it "signs out the user" do
+ within_user_menu do
+ find(".sign-out-link").click
+ end
+
+ expect(page).to have_content("Signed out successfully.")
+ expect(page).to have_no_content(user.name)
+ end
+ end
+
+ context "with lockable account" do
+ Devise.maximum_attempts = 3
+ let!(:maximum_attempts) { Devise.maximum_attempts }
+
+ describe "when attempting to login with failing password" do
+ describe "before locking" do
+ before do
+ visit decidim.root_path
+ find(".sign-in-link").click
+
+ (maximum_attempts - 2).times do
+ within ".new_user" do
+ fill_in :session_user_email, with: user.email
+ fill_in :session_user_password, with: "not-the-pasword"
+ find("*[type=submit]").click
+ end
+ end
+ end
+
+ it "shows the last attempt warning before locking the account" do
+ within ".new_user" do
+ fill_in :session_user_email, with: user.email
+ fill_in :session_user_password, with: "not-the-pasword"
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_content("You have one more attempt before your account is locked.")
+ end
+ end
+
+ describe "locks the account" do
+ before do
+ visit decidim.root_path
+ find(".sign-in-link").click
+
+ (maximum_attempts - 1).times do
+ within ".new_user" do
+ fill_in :session_user_email, with: user.email
+ fill_in :session_user_password, with: "not-the-pasword"
+ find("*[type=submit]").click
+ end
+ end
+ end
+
+ it "when reached maximum failed attempts" do
+ within ".new_user" do
+ fill_in :session_user_email, with: user.email
+ fill_in :session_user_password, with: "not-the-pasword"
+ perform_enqueued_jobs { find("*[type=submit]").click }
+ end
+
+ expect(page).to have_content("Your account is locked.")
+ expect(emails.count).to eq(1)
+ end
+ end
+ end
+
+ describe "Resend unlock instructions email" do
+ before do
+ user.lock_access!
+
+ visit decidim.new_user_unlock_path
+ end
+
+ it "resends the unlock instructions" do
+ within ".new_user" do
+ fill_in :unlock_user_email, with: user.email
+ perform_enqueued_jobs { find("*[type=submit]").click }
+ end
+
+ expect(page).to have_content("You will receive an email with instructions for how to unlock your account in a few minutes.")
+ expect(emails.count).to eq(1)
+ end
+ end
+
+ describe "Unlock account" do
+ before do
+ user.lock_access!
+ perform_enqueued_jobs { user.send_unlock_instructions }
+ end
+
+ it "unlocks the user account" do
+ visit last_email_link
+
+ expect(page).to have_content("Your account has been successfully unlocked. Please sign in to continue")
+ end
+ end
+ end
+ end
+
+ context "when a user is already registered with a social provider" do
+ let(:user) { create(:user, :confirmed, organization: organization) }
+ let(:identity) { create(:identity, user: user, provider: "facebook", uid: "12345") }
+
+ let(:omniauth_hash) do
+ OmniAuth::AuthHash.new(
+ provider: identity.provider,
+ uid: identity.uid,
+ info: {
+ email: user.email,
+ name: "Facebook User",
+ verified: true
+ }
+ )
+ end
+
+ before do
+ OmniAuth.config.test_mode = true
+ OmniAuth.config.mock_auth[:facebook] = omniauth_hash
+ end
+
+ after do
+ OmniAuth.config.test_mode = false
+ OmniAuth.config.mock_auth[:facebook] = nil
+ end
+
+ describe "Sign in" do
+ it "authenticates an existing User" do
+ find(".sign-in-link").click
+
+ click_link "Sign in with Facebook"
+
+ expect(page).to have_content("Successfully")
+ expect(page).to have_content(user.name)
+ end
+
+ context "when sign up is disabled" do
+ let(:organization) { create(:organization, users_registration_mode: :existing) }
+
+ it "doesn't allow the user to sign up" do
+ find(".sign-in-link").click
+ expect(page).not_to have_content("Sign Up")
+ end
+ end
+
+ context "when sign in is disabled" do
+ let(:organization) { create(:organization, users_registration_mode: :disabled) }
+
+ it "doesn't allow the user to sign up" do
+ find(".sign-in-link").click
+ expect(page).not_to have_content("Sign Up")
+ end
+
+ it "doesn't allow the user to sign in as a regular user, only through external accounts" do
+ find(".sign-in-link").click
+ expect(page).not_to have_content("Email")
+ expect(page).to have_css(".button--facebook")
+ end
+
+ it "authenticates an existing User" do
+ find(".sign-in-link").click
+
+ click_link "Sign in with Facebook"
+
+ expect(page).to have_content("Successfully")
+ expect(page).to have_content(user.name)
+ end
+ end
+ end
+ end
+
+ context "when a user is already registered in another organization with the same email" do
+ let(:user) { create(:user, :confirmed, password: "DfyvHn425mYAy2HL") }
+
+ describe "Sign Up" do
+ context "when using the same email" do
+ it "creates a new User" do
+ find(".sign-up-link").click
+
+ within ".new_user" do
+ fill_in :registration_user_email, with: user.email
+ fill_in :registration_user_name, with: "Responsible Citizen"
+ fill_in :registration_user_nickname, with: "responsible"
+ fill_in :registration_user_password, with: "DfyvHn425mYAy2HL"
+ fill_in :registration_user_password_confirmation, with: "DfyvHn425mYAy2HL"
+ check :registration_user_tos_agreement
+ check :registration_user_newsletter
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_content("You have signed up successfully")
+ end
+ end
+ end
+ end
+
+ context "when a user is already registered in another organization with the same fb account" do
+ let(:user) { create(:user, :confirmed) }
+ let(:identity) { create(:identity, user: user, provider: "facebook", uid: "12345") }
+
+ let(:omniauth_hash) do
+ OmniAuth::AuthHash.new(
+ provider: identity.provider,
+ uid: identity.uid,
+ info: {
+ email: user.email,
+ name: "Facebook User",
+ verified: true
+ }
+ )
+ end
+
+ before do
+ OmniAuth.config.test_mode = true
+ OmniAuth.config.mock_auth[:facebook] = omniauth_hash
+ end
+
+ after do
+ OmniAuth.config.test_mode = false
+ OmniAuth.config.mock_auth[:facebook] = nil
+ end
+
+ describe "Sign Up" do
+ context "when the user has confirmed the email in facebook" do
+ it "creates a new User without sending confirmation instructions" do
+ find(".sign-up-link").click
+
+ click_link "Sign in with Facebook"
+
+ expect(page).to have_content("Successfully")
+ expect_user_logged
+ end
+ end
+ end
+ end
+
+ context "when a user with the same email is already registered in another organization" do
+ let(:organization2) { create(:organization) }
+
+ let!(:user2) { create(:user, :confirmed, email: "fake@user.com", name: "Wrong user", organization: organization2, password: "DfyvHn425mYAy2HL") }
+ let!(:user) { create(:user, :confirmed, email: "fake@user.com", name: "Right user", organization: organization, password: "DfyvHn425mYAy2HL") }
+
+ describe "Sign in" do
+ it "authenticates the right user" do
+ find(".sign-in-link").click
+
+ within ".new_user" do
+ fill_in :session_user_email, with: user.email
+ fill_in :session_user_password, with: "DfyvHn425mYAy2HL"
+ find("*[type=submit]").click
+ end
+
+ expect(page).to have_content("successfully")
+ expect(page).to have_content("Right user")
+ end
+ end
+ end
+end
diff --git a/spec/system/decidim/homepage_spec.rb b/spec/system/decidim/homepage_spec.rb
new file mode 100644
index 0000000000..fad7f0cef3
--- /dev/null
+++ b/spec/system/decidim/homepage_spec.rb
@@ -0,0 +1,440 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe "Homepage", type: :system do
+ context "when there's no organization" do
+ before do
+ visit decidim.root_path
+ end
+
+ it "redirects to system UI and shows a warning" do
+ expect(page).to have_current_path(decidim_system.new_admin_session_path)
+ expect(page).to have_content("You must create an organization to get started")
+ end
+ end
+
+ context "when there's an organization" do
+ let(:official_url) { "http://mytesturl.me" }
+ let(:organization) { create(:organization, official_url: official_url) }
+
+ before do
+ create :content_block, organization: organization, scope_name: :homepage, manifest_name: :hero
+ create :content_block, organization: organization, scope_name: :homepage, manifest_name: :sub_hero
+ create :content_block, organization: organization, scope_name: :homepage, manifest_name: :highlighted_content_banner
+ create :content_block, organization: organization, scope_name: :homepage, manifest_name: :how_to_participate
+ create :content_block, organization: organization, scope_name: :homepage, manifest_name: :footer_sub_hero
+
+ switch_to_host(organization.host)
+ end
+
+ it_behaves_like "editable content for admins" do
+ let(:target_path) { visit decidim.root_path }
+ end
+
+ context "when requesting the root path" do
+ before do
+ visit decidim.root_path
+ end
+
+ it "includes the official organization links and images" do
+ expect(page).to have_selector("a.logo-cityhall[href='#{official_url}']")
+ expect(page).to have_selector("a.main-footer__badge[href='#{official_url}']")
+ end
+
+ context "and the organization has the omnipresent banner enabled" do
+ let(:organization) do
+ create(:organization,
+ official_url: official_url,
+ enable_omnipresent_banner: true,
+ omnipresent_banner_url: "#{official_url}/processes",
+ omnipresent_banner_title: Decidim::Faker::Localized.sentence(word_count: 3),
+ omnipresent_banner_short_description: Decidim::Faker::Localized.sentence(word_count: 3))
+ end
+
+ before do
+ switch_to_host(organization.host)
+ visit decidim.root_path
+ end
+
+ it "shows the omnipresent banner's title" do
+ expect(page).to have_i18n_content(organization.omnipresent_banner_title)
+ end
+
+ it "shows the omnipresent banner's short description" do
+ expect(page).to have_i18n_content(organization.omnipresent_banner_short_description)
+ end
+ end
+
+ describe "call to action" do
+ let!(:participatory_process) { create :participatory_process, :published }
+ let!(:organization) { participatory_process.organization }
+
+ before do
+ switch_to_host(organization.host)
+ visit decidim.root_path
+ end
+
+ context "when the organization has the CTA button text customized" do
+ let(:cta_button_text) { { en: "Sign up", es: "Regístrate", ca: "Registra't" } }
+ let(:organization) { create(:organization, cta_button_text: cta_button_text) }
+
+ it "uses the custom values for the CTA button text" do
+ within ".hero" do
+ expect(page).to have_selector("a.hero-cta", text: "SIGN UP")
+ click_link "Sign up"
+ end
+
+ expect(page).to have_current_path decidim.new_user_registration_path
+ end
+ end
+
+ context "when the organization has the CTA button link customized" do
+ let(:organization) { create(:organization, cta_button_path: "users/sign_in") }
+
+ it "uses the custom values for the CTA button" do
+ within ".hero" do
+ expect(page).to have_selector("a.hero-cta", text: "PARTICIPATE")
+ click_link "Participate"
+ end
+
+ expect(page).to have_current_path decidim.new_user_session_path
+ expect(page).to have_content("Sign in")
+ expect(page).to have_content("New to the platform?")
+ end
+ end
+
+ context "when the organization does not have it customized" do
+ it "uses the default values for the CTA button" do
+ visit decidim.root_path
+
+ within ".hero" do
+ expect(page).to have_selector("a.hero-cta", text: "PARTICIPATE")
+ click_link "Participate"
+ end
+
+ expect(page).to have_current_path decidim_participatory_processes.participatory_processes_path
+ end
+ end
+ end
+
+ context "with header snippets" do
+ let(:snippet) { "" }
+ let(:organization) { create(:organization, official_url: official_url, header_snippets: snippet) }
+
+ it "does not include the header snippets" do
+ expect(page).not_to have_selector("meta[data-hello]", visible: :all)
+ end
+
+ context "when header snippets are enabled" do
+ before do
+ allow(Decidim).to receive(:enable_html_header_snippets).and_return(true)
+ visit decidim.root_path
+ end
+
+ it "includes the header snippets" do
+ expect(page).to have_selector("meta[data-hello]", visible: :all)
+ end
+ end
+ end
+
+ it "welcomes the user" do
+ expect(page).to have_content(organization.name)
+ end
+
+ context "when there are static pages" do
+ let!(:static_page_1) { create(:static_page, organization: organization, show_in_footer: true) }
+ let!(:static_page_2) { create(:static_page, organization: organization, show_in_footer: true) }
+ let!(:static_page_3) { create(:static_page, organization: organization, show_in_footer: false) }
+
+ before do
+ visit current_path
+ end
+
+ it "includes links to them" do
+ within ".main-footer" do
+ [static_page_1, static_page_2].each do |static_page|
+ expect(page).to have_content(static_page.title["en"])
+ end
+
+ expect(page).to have_no_content(static_page_3.title["en"])
+ end
+
+ click_link static_page_1.title["en"]
+ expect(page).to have_i18n_content(static_page_1.title)
+
+ expect(page).to have_i18n_content(static_page_1.content)
+ end
+
+ it "includes the footer sub_hero with the current organization name" do
+ expect(page).to have_css(".footer__subhero")
+
+ within ".footer__subhero" do
+ expect(page).to have_content(organization.name)
+ end
+ end
+
+ context "when organization forces users to authenticate before access" do
+ let(:organization) do
+ create(
+ :organization,
+ official_url: official_url,
+ force_users_to_authenticate_before_access_organization: true
+ )
+ end
+ let(:user) { nil }
+ let!(:static_page_1) { create(:static_page, organization: organization, show_in_footer: true, allow_public_access: true) }
+ let!(:static_page_topic1) { create(:static_page_topic, organization: organization, show_in_footer: true) }
+ let!(:static_page_topic1_page1) do
+ create(
+ :static_page,
+ organization: organization,
+ topic: static_page_topic1,
+ weight: 0,
+ allow_public_access: false
+ )
+ end
+ let!(:static_page_topic1_page2) do
+ create(
+ :static_page,
+ organization: organization,
+ topic: static_page_topic1,
+ weight: 1,
+ allow_public_access: true
+ )
+ end
+ let!(:static_page_topic2) { create(:static_page_topic, organization: organization, show_in_footer: true) }
+ let!(:static_page_topic2_page1) { create(:static_page, organization: organization, topic: static_page_topic2, weight: 0) }
+ let!(:static_page_topic2_page2) { create(:static_page, organization: organization, topic: static_page_topic2, weight: 1) }
+ let!(:static_page_topic3) { create(:static_page_topic, organization: organization) }
+ let!(:static_page_topic3_page1) { create(:static_page, organization: organization, topic: static_page_topic3) }
+
+ # Re-visit required for the added pages and topics to be visible and
+ # to sign in the user when it is defined.
+ before do
+ login_as user, scope: :user if user
+ visit current_path
+ end
+
+ it "displays only publicly accessible pages and topics in the footer" do
+ within ".main-footer" do
+ expect(page).to have_content(static_page_1.title["en"])
+ expect(page).to have_no_content(static_page_2.title["en"])
+ expect(page).to have_no_content(static_page_3.title["en"])
+ expect(page).to have_content(static_page_topic1.title["en"])
+ expect(page).to have_no_content(static_page_topic2.title["en"])
+ expect(page).to have_no_content(static_page_topic3.title["en"])
+
+ expect(page).to have_link(
+ static_page_topic1.title["en"],
+ href: "/pages/#{static_page_topic1_page2.slug}"
+ )
+ end
+ end
+
+ context "when authenticated" do
+ let(:user) { create :user, :confirmed, organization: organization }
+
+ it "displays all pages and topics in footer that are configured to display in footer" do
+ expect(page).to have_content(static_page_1.title["en"])
+ expect(page).to have_content(static_page_2.title["en"])
+ expect(page).to have_no_content(static_page_3.title["en"])
+ expect(page).to have_content(static_page_topic1.title["en"])
+ expect(page).to have_content(static_page_topic2.title["en"])
+ expect(page).to have_no_content(static_page_topic3.title["en"])
+
+ expect(page).to have_link(
+ static_page_topic1.title["en"],
+ href: "/pages/#{static_page_topic1_page1.slug}"
+ )
+ expect(page).to have_link(
+ static_page_topic2.title["en"],
+ href: "/pages/#{static_page_topic2_page1.slug}"
+ )
+ end
+ end
+ end
+ end
+
+ describe "includes statistics" do
+ let!(:users) { create_list(:user, 4, :confirmed, organization: organization) }
+ let!(:participatory_process) do
+ create_list(
+ :participatory_process,
+ 2,
+ :published,
+ organization: organization,
+ description: { en: "Description", ca: "Descripció", es: "Descripción" },
+ short_description: { en: "Short description", ca: "Descripció curta", es: "Descripción corta" }
+ )
+ end
+
+ context "when organization doesn't have the stats content block" do
+ let(:organization) { create(:organization) }
+
+ it "does not show the statistics block" do
+ expect(page).to have_no_content("Current state of #{organization.name}")
+ end
+ end
+
+ context "when organization has the stats content block" do
+ let(:organization) { create(:organization) }
+
+ before do
+ create :content_block, organization: organization, scope_name: :homepage, manifest_name: :stats
+ visit current_path
+ end
+
+ it "shows the statistics block" do
+ within "#statistics" do
+ expect(page).to have_content("Current state of #{organization.name}")
+ expect(page).to have_content("PROCESSES")
+ expect(page).to have_content("PARTICIPANTS")
+ end
+ end
+
+ it "has the correct values for the statistics" do
+ within ".users_count" do
+ expect(page).to have_content("4")
+ end
+
+ within ".processes_count" do
+ expect(page).to have_content("2")
+ end
+ end
+ end
+ end
+
+ describe "includes metrics" do
+ context "when organization doesn't have the metrics content block" do
+ let(:organization) { create(:organization) }
+
+ it "does not show the statistics block" do
+ expect(page).to have_no_content("Participation in figures")
+ end
+ end
+
+ context "when organization does have the metrics content block" do
+ let(:organization) { create(:organization) }
+ let(:metrics) do
+ Decidim.metrics_registry.all.each do |metric_registry|
+ create(:metric, metric_type: metric_registry.metric_name, day: Time.zone.today, organization: organization, cumulative: 5, quantity: 2)
+ end
+ end
+
+ context "and have metric records" do
+ before do
+ metrics
+ create :content_block, organization: organization, scope_name: :homepage, manifest_name: :metrics
+ visit current_path
+ end
+
+ it "shows the metrics block" do
+ within "#metrics" do
+ expect(page).to have_content("Metrics")
+ Decidim.metrics_registry.filtered(highlight: true, scope: "home").each do |metric_registry|
+ expect(page).to have_css(%(##{metric_registry.metric_name}_chart), visible: :all)
+ end
+ Decidim.metrics_registry.filtered(highlight: false, scope: "home").each do |metric_registry|
+ expect(page).to have_css(%(##{metric_registry.metric_name}_chart), visible: :all)
+ end
+ end
+ end
+ end
+
+ context "and does not have metric records" do
+ before do
+ create :content_block, organization: organization, scope_name: :homepage, manifest_name: :metrics
+ visit current_path
+ end
+
+ it "shows the metrics block empty" do
+ within "#metrics" do
+ expect(page).to have_content("Metrics")
+ Decidim.metrics_registry.highlighted.each do |metric_registry|
+ expect(page).to have_no_css("##{metric_registry.metric_name}_chart")
+ end
+ Decidim.metrics_registry.not_highlighted.each do |metric_registry|
+ expect(page).to have_no_css("##{metric_registry.metric_name}_chart")
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe "social links" do
+ before do
+ organization.update(
+ twitter_handler: "twitter_handler",
+ facebook_handler: "facebook_handler",
+ youtube_handler: "youtube_handler",
+ github_handler: "github_handler"
+ )
+
+ visit current_path
+ end
+
+ it "includes the links to social networks" do
+ expect(page).to have_xpath("//a[@href = 'https://twitter.com/twitter_handler']")
+ expect(page).to have_xpath("//a[@href = 'https://www.facebook.com/facebook_handler']")
+ expect(page).to have_xpath("//a[@href = 'https://www.youtube.com/youtube_handler']")
+ expect(page).to have_xpath("//a[@href = 'https://www.github.com/github_handler']")
+ end
+ end
+
+ context "and has highlighted content banner enabled" do
+ let(:organization) do
+ create(:organization,
+ official_url: official_url,
+ highlighted_content_banner_enabled: true,
+ highlighted_content_banner_title: Decidim::Faker::Localized.sentence(word_count: 2),
+ highlighted_content_banner_short_description: Decidim::Faker::Localized.sentence(word_count: 2),
+ highlighted_content_banner_action_title: Decidim::Faker::Localized.sentence(word_count: 2),
+ highlighted_content_banner_action_subtitle: Decidim::Faker::Localized.sentence(word_count: 2),
+ highlighted_content_banner_action_url: ::Faker::Internet.url,
+ highlighted_content_banner_image: Decidim::Dev.test_file("city.jpeg", "image/jpeg"))
+ end
+
+ before do
+ switch_to_host(organization.host)
+ visit decidim.root_path
+ end
+
+ it "shows the banner's title" do
+ expect(page).to have_i18n_content(organization.highlighted_content_banner_title)
+ end
+
+ it "shows the banner's description" do
+ expect(page).to have_i18n_content(organization.highlighted_content_banner_short_description)
+ end
+
+ it "shows the banner's action title" do
+ expect(page).to have_i18n_content(organization.highlighted_content_banner_action_title, upcase: true)
+ end
+
+ it "shows the banner's action subtitle" do
+ expect(page).to have_i18n_content(organization.highlighted_content_banner_action_subtitle)
+ end
+ end
+
+ context "when downloading open data", download: true do
+ before do
+ Decidim::OpenDataJob.perform_now(organization)
+ switch_to_host(organization.host)
+ visit decidim.root_path
+ end
+
+ it "lets the users download open data files" do
+ click_link "Open data"
+ expect(File.basename(download_path)).to include("open-data.zip")
+ Zip::File.open(download_path) do |zipfile|
+ expect(zipfile.glob("*open-data-proposals.csv").length).to eq(1)
+ expect(zipfile.glob("*open-data-results.csv").length).to eq(1)
+ expect(zipfile.glob("*open-data-meetings.csv").length).to eq(1)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/system/decidim/user_tos_acceptance_spec.rb b/spec/system/decidim/user_tos_acceptance_spec.rb
new file mode 100644
index 0000000000..3c53271e86
--- /dev/null
+++ b/spec/system/decidim/user_tos_acceptance_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe "UserTosAcceptance", type: :system do
+ let!(:organization) { create(:organization) }
+ let!(:user) { create(:user, :confirmed, organization: organization) }
+ let!(:tos_page) { Decidim::StaticPage.find_by(slug: "terms-and-conditions", organization: organization) }
+ let(:btn_accept) { "I agree with these terms" }
+ let(:btn_refuse) { "Refuse the terms" }
+
+ before do
+ switch_to_host(organization.host)
+ end
+
+ describe "When the Organization TOS version is updated" do
+ before do
+ organization.update!(tos_version: Faker::Date.forward(days: 15))
+ login_as user, scope: :user
+ visit decidim.root_path
+ end
+
+ context "when a user starts a session, has to accept and review them" do
+ it "redirects to the TOS page" do
+ expect(page).to have_current_path(decidim.page_path(tos_page))
+ expect(page).to have_content translated(tos_page.title)
+ expect(page.find(".card__content p", obscured: false)).to have_content strip_tags(translated(tos_page.content))
+ end
+
+ it "renders an announcement requiring to review the TOS" do
+ expect(page).to have_content("Required: Review updates to our terms of service")
+ end
+
+ it "renders an announcement advising that TOS has been updated" do
+ expect(page).to have_content("We've updated our Terms of Service, please review them.")
+ end
+
+ it "shows a button to Agree the updated Terms" do
+ expect(page).to have_button btn_accept
+ end
+
+ it "shows a button to Refuse Terms" do
+ expect(page).to have_button btn_refuse
+ end
+ end
+
+ context "and the user accepts the updated TOS" do
+ before do
+ click_button btn_accept
+ end
+
+ it "renders a success announcement" do
+ expect(page).to have_content("Great! You have accepted the terms and conditions.")
+ expect(page).to have_css(".flash.success")
+ end
+ end
+
+ context "and the user refuses the updated TOS" do
+ before do
+ click_button btn_refuse
+ end
+
+ it "renders a modal" do
+ expect(page).to have_css("#tos-refuse-modal")
+ expect(page).to have_content("Do you really refuse the updated Terms and Conditions?")
+ end
+
+ context "with the refuse modal has different options" do
+ it "shows an option to accept the TOS" do
+ within "#tos-refuse-modal" do
+ expect(page).to have_button("Accept terms and continue")
+ expect(page).to have_tag("form", action: decidim.accept_tos_path)
+ end
+ end
+
+ it "shows an option to download the users data" do
+ within "#tos-refuse-modal" do
+ expect(page).to have_link("download your data", href: decidim.data_portability_path)
+ end
+ end
+
+ it "shows an option to logout" do
+ within "#tos-refuse-modal" do
+ expect(page).to have_button("I'll review it later")
+ expect(page).to have_tag("form", action: decidim.destroy_user_session_path)
+ end
+ end
+
+ it "shows an option delete the account" do
+ within "#tos-refuse-modal" do
+ expect(page).to have_link("delete your account", href: decidim.delete_account_path)
+ end
+ end
+ end
+ end
+ end
+end