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 @@
- + +
+ <%= render partial: "layouts/decidim/social_media_links" %> + diff --git a/app/views/layouts/decidim/_wrapper.html.erb b/app/views/layouts/decidim/_wrapper.html.erb index 4093d98214..00c314454b 100644 --- a/app/views/layouts/decidim/_wrapper.html.erb +++ b/app/views/layouts/decidim/_wrapper.html.erb @@ -33,6 +33,7 @@ end creates a sticky footer-->
+ <%= link_to t("skip_button", scope: "decidim.accessibility"), url_for(anchor: "content"), class: "skip" %> <% if current_organization.official_img_header? %> <%= link_to current_organization.official_url, class: "logo-cityhall" do %> <%= image_tag current_organization.official_img_header.url.to_s , alt: current_organization.name %> @@ -45,33 +46,37 @@ end <%= render partial: "layouts/decidim/topbar_search" %> <%= render partial: "layouts/decidim/language_chooser" %>
-
<% if current_user %> -
- <%= link_to decidim.notifications_path, class: "topbar__notifications #{current_user.notifications.any? ? "is-active" : ""}", "aria-label": t("layouts.decidim.user_menu.notifications") do %> - <%= icon "bell" %> +
+ <% else %>
- +
-
+
<%= 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