diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..565cbcd6f6 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,48 @@ +# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view +# Remove commits converting files to 2-spaces +e9074d60ea28f9fb98e82be4fe15d0de5d637eff +3fbd060cea72f717df8b0651a69c79f4fdd509f8 +81c0cf2d2eb2dcccbdbad9dc8937793dc244424b +3fb4bade8546052f69f68311ff3b5eb530bdce06 +137cb672f1532f916239969756f2392337f1c8df +4eee188e093bc8bf3782ec4bd14c3529b0b01586 +e58feafc6ab8a9b6d51946c55cde6adae9116568 +390c000ab8f3dd081db7feced594a3798cdf2147 +83429e1295b90e684cd67108737d6dabf80ae883 +c58f6013d9c374664dffbfa645fb8fed60d335d8 +5dec2b34f0512dadc470a8d006e46d260881a2ca +423a748e925c0e196a540197594f289f8a948648 +f2dc73ce4e676a390205bafd6732a217f3de3c89 +0b498ff3b0fe95496d90fd63be718e895a24537c +ec07eae1d3e4ce65cad2d5cbe5221a89e06e45af +423ef61c3df718df6bb2a9b0dbf5d5c7180230cd +9202518ad9018182680fca33902d63a825c3fe16 +cefe07dee896adb7aa542601b3f7cc75f990df6c +953b9e225b2029d75d8f864d7a038aeb0860b32d +7fbd7299e238e41cc341bc3e6d65ed757d4e870a +9e556bcfa67c6cb4843c5b5398c96f84c4639708 +0da7b93c5f6e3ebff376e5b5dd01c9f03ec1c2c8 +19a3591d6246dc3af866982a1fe8d6d609d8514b +bb60b2632117e692f933810dbf321f22d27604fd +38da000ae7b6d11c620f28866715922ef980360b +92563edca5d7552235c9dd069aabc375be9c5b74 +872dd47ca574ed8b06597e49be4d21712e8a6a0b +b5caf1f17165c597b00020805c909b1eb8907e97 +844453b95c497b810fc27005ce376a90ea754fa9 +c85fd8205c2f5eab598de9a14d1acda4f3380992 +b805700e3e632be1dcbea4d96a667722129d2c2b +30681a0a22b49dfff2af6139472757dce3d2c040 +cf020f3bce0dc6525b6a8a45258ddb55448be17c +db492303b1d2a22de4be039cde0961c48761d7ce +f07280eeb98f6058b75a3ca94afb73254c7963c7 +d35e92ba521b1183bf644e03aad8bbea721d9894 +ce8b44f8669b276767e03965865e4b8411e0abd3 +4be25b1ca7754f8b2ad2aef8b1010911d1705dc6 +8d0a624458733bb6b84c9185e3cc6dd087fbe087 +b0aac8fc3d038553ca13fdd9cae3a85ae6a2a6d8 +b80cc391530136d6178f0539100aef7c78a63a63 +da75289f3414b498f0708b24bbdfc09864fe2da5 +29aeb19078f68e72ac781fbe27607a744d2b712a +6691f97656f1aff03b3da8cdf42c94eb05d9fc07 +0b4059b3f3a4898526b7c31d5f4db6429b9a26ef +f2c538c1a25d91de33dd583a5ee34e433ffaa8a6 diff --git a/.github/git-io-urls.md b/.github/git-io-urls.md new file mode 100644 index 0000000000..6eccc6e1af --- /dev/null +++ b/.github/git-io-urls.md @@ -0,0 +1,26 @@ +# git.io URL shortener URLs + +GitHub [announced it was shutting down git.io](https://github.blog/changelog/2022-04-25-git-io-deprecation) +on Friday, April 29, 2022 and as a consequence all links on +[git.io](https://git.io) will stop redirecting. + +We've replaced these URLs in the source code and GitHub issues where possible. + +## Commit Messages + +Commit messages can't be changed, so this table acts as a manual lookup for +git.io URLs known to be included in commits in Alaveteli. + +```csv +https://git.io/JvMcI,https://github.com/rails/rails/blob/v5.1.7/actionview/lib/action_view/helpers/javascript_helper.rb#L25-L32 +https://git.io/JvMcL,https://github.com/rails/rails/blob/v5.2.4.1/actionview/lib/action_view/helpers/javascript_helper.rb#L27-L34 +https://git.io/JvMcm,https://github.com/rails/rails/commit/b5aeef5703dab7da9ebb47cc20e4c8b64f7f5866 +https://git.io/hR7f,https://github.com/alexdunae/holidays/blob/master/CHANGELOG.md#120 +https://git.io/v0QN2,https://github.com/holidays/holidays/blob/master/CHANGELOG.md#220 +https://git.io/v6otV,https://github.com/svenfuchs/routing-filter/issues/47#issue-14760397 +https://git.io/vKpwO,https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543 +https://git.io/vozPG,https://github.com/mikel/mail/blob/a217776355befa3d8191c4bd3c1fad54e0e27471/lib/mail/version_specific/ruby_1_9.rb#L89 +https://git.io/vun4e,https://github.com/travis-ci/travis-ci/issues/1242#issuecomment-21660547 +https://git.io/vvv93,https://github.com/mysociety/alaveteli/blob/8c72a2590a6c0a5f21491b96f44eeb8da53663bd/spec/integration/admin_public_body_edit_spec.rb#L48 +https://git.io/vvv9t,https://github.com/mysociety/alaveteli/blob/8c72a2590a6c0a5f21491b96f44eeb8da53663bd/spec/integration/admin_public_body_edit_spec.rb#L42-L70 +``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84b17f151d..51486cb02c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,23 +5,31 @@ on: branches: [master, develop] pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + jobs: rspec: - name: Ruby ${{ matrix.ruby }} / ${{ matrix.gemfile || 'Gemfile' }} + name: Ruby ${{ matrix.ruby }} / PostgreSQL ${{ matrix.postgres }} / ${{ matrix.gemfile || 'Gemfile' }} runs-on: ubuntu-20.04 + permissions: + checks: write # for coverallsapp/github-action to create new checks + strategy: fail-fast: false matrix: include: - - { ruby: 2.5 } - - { ruby: 2.6 } - - { ruby: 2.7 } - - { ruby: 2.7, gemfile: 'Gemfile.rails_next' } + - { ruby: 2.7, postgres: 13.5 } + - { ruby: 2.7, postgres: 13.5, gemfile: 'Gemfile.rails_next' } services: postgres: - image: fixmystreet/postgres:latest + image: fixmystreet/postgres:${{ matrix.postgres }} env: POSTGRES_PASSWORD: postgres ports: @@ -42,6 +50,7 @@ jobs: uses: actions/checkout@v2 with: submodules: true + fetch-depth: 0 - name: Install packages env: @@ -55,7 +64,6 @@ jobs: uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - bundler: ${{ matrix.bundler || '2.1.4' }} bundler-cache: true - name: Setup database @@ -66,9 +74,13 @@ jobs: CREATE DATABASE alaveteli_test TEMPLATE template_utf8; EOSQL - - name: Install theme + - name: Configure application and storage run: | cp config/general.yml-example config/general.yml + cp config/storage.yml-example config/storage.yml + + - name: Install theme + run: | bundle exec rake themes:install - name: Migrate database @@ -88,6 +100,8 @@ jobs: parallel: true coveralls: + permissions: + checks: write name: Coveralls needs: rspec runs-on: ubuntu-20.04 diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index f255a60310..3a3757a775 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -2,6 +2,9 @@ name: RuboCop on: [pull_request] +permissions: + contents: read + jobs: build: runs-on: ubuntu-20.04 @@ -11,7 +14,7 @@ jobs: - name: Install Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.6 + ruby-version: 2.7 - name: Run RuboCop linter uses: reviewdog/action-rubocop@v1 diff --git a/.gitignore b/.gitignore index 82cd9588ad..8886015edc 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ /config/memcached.yml /config/newrelic.yml /config/rails_env.rb +/config/storage.yml /config/user_spam_scorer.yml /config/xapian.yml /coverage/ @@ -39,6 +40,7 @@ /public/foi-user-use.png /public/google*.html /public/wordpress +/storage/ /tmp/ /vendor/bundle/ /vendor/data/ diff --git a/.ruby-style.yml b/.ruby-style.yml index 639c80ca83..bcb5015442 100644 --- a/.ruby-style.yml +++ b/.ruby-style.yml @@ -4,7 +4,7 @@ require: - rubocop-rails AllCops: - TargetRubyVersion: 2.5 + TargetRubyVersion: 2.7 RubyInterpreters: - ruby - rake @@ -53,12 +53,21 @@ Bundler/OrderedGems: Gemspec/DateAssignment: Enabled: false +Gemspec/DependencyVersion: + Enabled: false + +Gemspec/DeprecatedAttributeAssignment: + Enabled: false + Gemspec/DuplicatedAssignment: Enabled: false Gemspec/OrderedDependencies: Enabled: false +Gemspec/RequireMFA: + Enabled: false + Gemspec/RequiredRubyVersion: Enabled: false @@ -237,6 +246,14 @@ Layout/LineLength: - "^\\s*context\\s+.*do$" - "^\\s*describe\\s+.*do$" - "^\\s*class\\s+[A-Z].*<.*" + Exclude: + - bin/setup + - config/environments/development.rb + - config/environments/production.rb + - config/environments/test.rb + - config/initializers/backtrace_silencers.rb + - config/initializers/content_security_policy.rb + - config/initializers/new_framework_defaults* Layout/MultilineArrayBraceLayout: Enabled: true @@ -621,6 +638,9 @@ Lint/RedundantWithIndex: Lint/RedundantWithObject: Enabled: false +Lint/RefinementImportMethods: + Enabled: false + Lint/RegexpAsCondition: Enabled: false @@ -729,10 +749,10 @@ Lint/UselessAccessModifier: Lint/UselessAssignment: Enabled: false -Lint/UselessElseWithoutRescue: +Lint/UselessMethodDefinition: Enabled: false -Lint/UselessMethodDefinition: +Lint/UselessRuby2Keywords: Enabled: false Lint/UselessSetterCall: @@ -789,6 +809,9 @@ Naming/AsciiIdentifiers: Naming/BinaryOperatorParameterName: Enabled: false +Naming/BlockForwarding: + Enabled: false + Naming/BlockParameterName: Enabled: false @@ -959,6 +982,9 @@ Performance/Squeeze: Performance/StartWith: Enabled: false +Performance/StringIdentifierArgument: + Enabled: false + Performance/StringInclude: Enabled: false @@ -979,6 +1005,9 @@ Performance/UriDefaultParser: #################### Rails #################### +Rails/ActionControllerTestCase: + Enabled: false + Rails/ActionFilter: Enabled: false @@ -1030,6 +1059,9 @@ Rails/Blank: Rails/BulkChangeTable: Enabled: false +Rails/CompactBlank: + Enabled: false + Rails/ContentTag: Enabled: false @@ -1048,6 +1080,21 @@ Rails/Delegate: Rails/DelegateAllowBlank: Enabled: false +Rails/DeprecatedActiveModelErrorsMethods: + Enabled: false + +Rails/DotSeparatedKeys: + Enabled: false + +Rails/DuplicateAssociation: + Enabled: false + +Rails/DuplicateScope: + Enabled: false + +Rails/DurationArithmetic: + Enabled: false + Rails/DynamicFindBy: Enabled: false @@ -1099,9 +1146,15 @@ Rails/HttpPositionalArguments: Rails/HttpStatus: Enabled: false +Rails/I18nLazyLookup: + Enabled: false + Rails/I18nLocaleAssignment: Enabled: false +Rails/I18nLocaleTexts: + Enabled: false + Rails/IgnoredSkipActionFilterOption: Enabled: false @@ -1129,6 +1182,9 @@ Rails/MailerName: Rails/MatchRoute: Enabled: false +Rails/MigrationClassName: + Enabled: false + Rails/NegateInclude: Enabled: false @@ -1177,6 +1233,9 @@ Rails/RedundantAllowNil: Rails/RedundantForeignKey: Enabled: false +Rails/RedundantPresenceValidationOnBelongsTo: + Enabled: false + Rails/RedundantReceiverInWithOptions: Enabled: false @@ -1210,6 +1269,12 @@ Rails/ReversibleMigration: Rails/ReversibleMigrationMethodDefinition: Enabled: false +Rails/RootJoinChain: + Enabled: false + +Rails/RootPublicPath: + Enabled: false + Rails/SafeNavigation: Enabled: false @@ -1219,6 +1284,9 @@ Rails/SafeNavigationWithBlank: Rails/SaveBang: Enabled: false +Rails/SchemaComment: + Enabled: false + Rails/ScopeArgs: Enabled: false @@ -1231,12 +1299,24 @@ Rails/SkipsModelValidations: Rails/SquishedSQLHeredocs: Enabled: false +Rails/StripHeredoc: + Enabled: false + +Rails/TableNameAssignment: + Enabled: false + Rails/TimeZone: Enabled: false Rails/TimeZoneAssignment: Enabled: false +Rails/ToFormattedS: + Enabled: false + +Rails/TransactionExitStatement: + Enabled: false + Rails/UniqBeforePluck: Enabled: false @@ -1263,6 +1343,9 @@ Rails/WhereNot: #################### Security #################### +Security/CompoundHash: + Enabled: false + Security/Eval: Enabled: false @@ -1454,6 +1537,9 @@ Style/EndBlock: Style/EndlessMethod: Enabled: false +Style/EnvHome: + Enabled: false + Style/EvalWithLocation: Enabled: false @@ -1469,6 +1555,15 @@ Style/ExplicitBlockArgument: Style/ExponentialNotation: Enabled: false +Style/FetchEnvVar: + Enabled: false + +Style/FileRead: + Enabled: false + +Style/FileWrite: + Enabled: false + Style/FloatDivision: Enabled: false @@ -1569,6 +1664,12 @@ Style/LambdaCall: Style/LineEndConcatenation: Enabled: true +Style/MapCompactWithConditionalBlock: + Enabled: false + +Style/MapToHash: + Enabled: false + Style/MethodCallWithArgsParentheses: Enabled: false @@ -1641,6 +1742,9 @@ Style/NegatedUnless: Style/NegatedWhile: Enabled: true +Style/NestedFileDirname: + Enabled: false + Style/NestedModifier: Enabled: true @@ -1680,9 +1784,15 @@ Style/NumericLiterals: Style/NumericPredicate: Enabled: false +Style/ObjectThen: + Enabled: false + Style/OneLineConditional: Enabled: false +Style/OpenStructUse: + Enabled: false + Style/OptionHash: Enabled: false @@ -1755,6 +1865,9 @@ Style/RedundantFileExtensionInRequire: Style/RedundantFreeze: Enabled: true +Style/RedundantInitialize: + Enabled: false + Style/RedundantInterpolation: Enabled: true @@ -1860,6 +1973,18 @@ Style/StringHashKeys: Style/StringLiterals: Enabled: false + EnforcedStyle: single_quotes + Exclude: + - bin/rails + - bin/rake + - bin/setup + - config.ru + - config/application.rb + - config/boot.rb + - config/environment.rb + - config/environments/development.rb + - config/environments/production.rb + - config/environments/test.rb Style/StringLiteralsInInterpolation: Enabled: false diff --git a/.ruby-version.example b/.ruby-version.example index ecd7ee50cb..a4dd9dba4f 100644 --- a/.ruby-version.example +++ b/.ruby-version.example @@ -1 +1 @@ -2.5.8 +2.7.4 diff --git a/.vagrant.yml.example b/.vagrant.yml.example index 368b96250a..2e9751df20 100644 --- a/.vagrant.yml.example +++ b/.vagrant.yml.example @@ -4,7 +4,7 @@ ip: 10.10.10.30 public_network: false memory: 1536 themes_dir: ../alaveteli-themes -os: stretch64 +os: bullseye64 use_nfs: false show_settings: false # By default CPU count is calculated dynamically diff --git a/Gemfile b/Gemfile index a5f359a896..4fec996d65 100644 --- a/Gemfile +++ b/Gemfile @@ -84,43 +84,51 @@ def rails_upgrade? %w[1 true].include?(ENV['RAILS_UPGRADE']) end -gem 'rails', rails_upgrade? ? '~> 6.1.4' : '~> 6.0.3' +if rails_upgrade? + gem 'rails', '~> 7.0.3' +else + gem 'rails', '~> 6.1.6' +end -gem 'pg', '~> 1.2.3' +gem 'pg', '~> 1.4.1' # New gem releases aren't being done. master is newer and supports Rails > 3.0 gem 'acts_as_versioned', :git => 'https://github.com/technoweenie/acts_as_versioned.git', :ref => '63b1fc8529d028' gem 'active_model_otp' -gem 'bcrypt', '~> 3.1.16' -gem 'cancancan', '~> 3.3.0' +gem 'bcrypt', '~> 3.1.18' +gem 'cancancan', '~> 3.4.0' gem 'charlock_holmes', '~> 0.7.7' -gem 'dalli', '~> 3.0.4' -gem 'exception_notification', '~> 4.4.3' +gem 'dalli', '~> 3.2.2' +gem 'exception_notification', '~> 4.5.0' gem 'fancybox-rails', '~> 0.3.0' gem 'gnuplot', '~> 2.6.0' gem 'htmlentities', '~> 4.3.0' gem 'icalendar', '~> 2.7.1' -gem 'jquery-rails', '~> 4.4.0' +gem 'jquery-rails', '~> 4.5.0' gem 'jquery-ui-rails', '~> 6.0.0' -gem 'json', '~> 2.6.1' -gem 'holidays', '~> 8.4.1' +gem 'json', '~> 2.6.2' +gem 'holidays', '~> 8.5.0' gem 'iso_country_codes', '~> 0.7.8' gem 'mail', '~> 2.7.1' gem 'maxmind-db', '~> 1.0.0' gem 'mahoro', '~> 0.5' -gem 'nokogiri', '~> 1.12.5' +gem 'nokogiri', '~> 1.13.6' gem 'open4', '~> 1.3.0' gem 'rack', '~> 2.2.3' gem 'rack-utf8_sanitizer', '~> 1.7.0' -gem 'recaptcha', '~> 5.8.1', require: 'recaptcha/rails' +gem 'recaptcha', '~> 5.10.0', require: 'recaptcha/rails' gem 'mini_magick', '~> 4.11.0' gem 'rolify', '~> 5.3.0' gem 'ruby-msg', '~> 1.5.0', :git => 'https://github.com/mysociety/ruby-msg.git', :branch => 'ascii-encoding' gem 'rubyzip', '~> 2.3.2' gem 'secure_headers', '~> 6.3.3' gem 'statistics2', '~> 0.54' -gem 'strip_attributes', :git => 'https://github.com/mysociety/strip_attributes.git', :branch => 'globalize3-rails5.2' -gem 'stripe', '~> 5.39.0' +if rails_upgrade? + gem 'strip_attributes', :git => 'https://github.com/mysociety/strip_attributes.git', :branch => 'globalize3-rails7' +else + gem 'strip_attributes', :git => 'https://github.com/mysociety/strip_attributes.git', :branch => 'globalize3-rails5.2' +end +gem 'stripe', '~> 5.55.0' gem 'syslog_protocol', '~> 0.9.0' gem 'thin', '~> 1.8.1' gem 'vpim', '~> 13.11.11' @@ -133,14 +141,14 @@ gem 'zip_tricks', '~> 5.6.0' gem 'gender_detector', '~> 2.0.0' # Gems related to internationalisation -gem 'i18n', '~> 1.8.11' -gem 'rails-i18n', '~> 6.0.0' +gem 'i18n', '~> 1.10.0' +gem 'rails-i18n', '~> 7.0.3' gem 'gettext_i18n_rails', '~> 1.8.1' - gem 'fast_gettext', '~> 2.1.0' -gem 'gettext', '~> 3.4.1' -gem 'globalize', rails_upgrade? ? '~> 6.0.0' : '~> 5.3.0' + gem 'fast_gettext', '~> 2.2.0' +gem 'gettext', '~> 3.4.3' +gem 'globalize', '~> 6.2.1' gem 'locale', '~> 2.1.3' -gem 'routing-filter', rails_upgrade? ? '~> 0.7.0' : '~> 0.6.2' +gem 'routing-filter', '~> 0.7.0' gem 'unicode', '~> 0.4.4' gem 'unidecoder', '~> 1.1.0' gem 'money', '~> 6.16.0' @@ -150,43 +158,53 @@ gem 'mime-types', '< 3.0.0', require: false # Assets gem 'bootstrap-sass', '~> 2.3.2.2' -gem 'mini_racer', '~> 0.4.0' +gem 'mini_racer', '~> 0.6.2' gem 'sass-rails', '~> 5.0.8' gem 'uglifier', '~> 4.2.0' # Feature flags gem 'alaveteli_features', :path => 'gems/alaveteli_features' +# Storage backends +gem 'aws-sdk-s3', require: false +gem 'azure-storage', require: false +gem 'google-cloud-storage', '~> 1.36', require: false + +if rails_upgrade? && RUBY_VERSION < '3.1' + gem 'net-http', '0.1.1' + gem 'uri', '0.10.0' +end + group :test do gem 'fivemat', '~> 1.3.7' gem 'webmock', '~> 3.14.0' gem 'simplecov', '~> 0.17.1' gem 'simplecov-lcov', '~> 0.7.0' - gem 'capybara', '~> 3.35.3' + gem 'capybara', '~> 3.37.1' gem 'stripe-ruby-mock', git: 'https://github.com/stripe-ruby-mock/stripe-ruby-mock', ref: '2c925fd' gem('rails-controller-testing') end group :test, :development do - gem 'bullet', '~> 6.1.5' + gem 'bullet', '~> 7.0.2' gem 'factory_bot_rails', '~> 6.2.0' gem 'oink', '~> 0.10.1' gem 'rspec-activemodel-mocks', '~> 1.1.0' - gem 'rspec-rails', '~> 5.0.2' + gem 'rspec-rails', '~> 5.1.2' gem 'pry', '~> 0.13.0' gem 'pry-byebug', '~> 3.9.0' end group :development do - gem 'annotate', '< 3.1.1' + gem 'annotate', '< 3.2.1' gem 'capistrano', '~> 2.15.0', '< 3.0.0' gem 'net-ssh', '~> 6.1.0' gem 'net-ssh-gateway', '>= 1.1.0', '< 3.0.0' gem 'launchy', '< 2.5.0' - gem 'listen', '>= 3.0.5', '< 3.7.1' + gem 'listen', '>= 3.0.5', '< 3.7.2' gem 'web-console', '>= 3.3.0' - gem 'rubocop', '~> 1.22.3', require: false + gem 'rubocop', '~> 1.30.1', require: false gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index 4473e2e871..315d90ea50 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -40,43 +40,45 @@ PATH flipper (~> 0.10) flipper-active_record (~> 0.10) mime-types (< 3.0.0) - rails (~> 6.0.3) + rails (~> 6.1.4) GEM remote: https://rubygems.org/ specs: - actioncable (6.0.4.1) - actionpack (= 6.0.4.1) + actioncable (6.1.6) + actionpack (= 6.1.6) + activesupport (= 6.1.6) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.4.1) - actionpack (= 6.0.4.1) - activejob (= 6.0.4.1) - activerecord (= 6.0.4.1) - activestorage (= 6.0.4.1) - activesupport (= 6.0.4.1) + actionmailbox (6.1.6) + actionpack (= 6.1.6) + activejob (= 6.1.6) + activerecord (= 6.1.6) + activestorage (= 6.1.6) + activesupport (= 6.1.6) mail (>= 2.7.1) - actionmailer (6.0.4.1) - actionpack (= 6.0.4.1) - actionview (= 6.0.4.1) - activejob (= 6.0.4.1) + actionmailer (6.1.6) + actionpack (= 6.1.6) + actionview (= 6.1.6) + activejob (= 6.1.6) + activesupport (= 6.1.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.4.1) - actionview (= 6.0.4.1) - activesupport (= 6.0.4.1) - rack (~> 2.0, >= 2.0.8) + actionpack (6.1.6) + actionview (= 6.1.6) + activesupport (= 6.1.6) + rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.4.1) - actionpack (= 6.0.4.1) - activerecord (= 6.0.4.1) - activestorage (= 6.0.4.1) - activesupport (= 6.0.4.1) + actiontext (6.1.6) + actionpack (= 6.1.6) + activerecord (= 6.1.6) + activestorage (= 6.1.6) + activesupport (= 6.1.6) nokogiri (>= 1.8.5) - actionview (6.0.4.1) - activesupport (= 6.0.4.1) + actionview (6.1.6) + activesupport (= 6.1.6) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -84,49 +86,77 @@ GEM active_model_otp (2.3.1) activemodel rotp (~> 6.2.0) - activejob (6.0.4.1) - activesupport (= 6.0.4.1) + activejob (6.1.6) + activesupport (= 6.1.6) globalid (>= 0.3.6) - activemodel (6.0.4.1) - activesupport (= 6.0.4.1) - activerecord (6.0.4.1) - activemodel (= 6.0.4.1) - activesupport (= 6.0.4.1) - activestorage (6.0.4.1) - actionpack (= 6.0.4.1) - activejob (= 6.0.4.1) - activerecord (= 6.0.4.1) - marcel (~> 1.0.0) - activesupport (6.0.4.1) + activemodel (6.1.6) + activesupport (= 6.1.6) + activerecord (6.1.6) + activemodel (= 6.1.6) + activesupport (= 6.1.6) + activestorage (6.1.6) + actionpack (= 6.1.6) + activejob (= 6.1.6) + activerecord (= 6.1.6) + activesupport (= 6.1.6) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (6.1.6) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) - annotate (3.1.0) - activerecord (>= 3.2, < 7.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) - bcrypt (3.1.16) + aws-eventstream (1.2.0) + aws-partitions (1.590.0) + aws-sdk-core (3.131.1) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.525.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.57.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.114.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.5.0) + aws-eventstream (~> 1, >= 1.0.2) + azure-core (0.1.15) + faraday (~> 0.9) + faraday_middleware (~> 0.10) + nokogiri (~> 1.6) + azure-storage (0.15.0.preview) + azure-core (~> 0.1) + faraday (~> 0.9) + faraday_middleware (~> 0.10) + nokogiri (~> 1.6, >= 1.6.8) + bcrypt (3.1.18) bindex (0.8.1) bootstrap-sass (2.3.2.2) sass (~> 3.2) builder (3.2.4) - bullet (6.1.5) + bullet (7.0.2) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) byebug (11.1.3) - cancancan (3.3.0) + cancancan (3.4.0) capistrano (2.15.9) highline net-scp (>= 1.0.0) net-sftp (>= 2.0.0) net-ssh (>= 2.0.14) net-ssh-gateway (>= 1.1.0) - capybara (3.35.3) + capybara (3.37.1) addressable + matrix mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) @@ -135,20 +165,23 @@ GEM xpath (~> 3.2) charlock_holmes (0.7.7) coderay (1.1.3) - concurrent-ruby (1.1.9) + concurrent-ruby (1.1.10) crack (0.4.5) rexml crass (1.0.6) daemons (1.4.0) - dalli (3.0.4) + dalli (3.2.2) dante (0.2.0) - diff-lcs (1.4.4) + declarative (0.0.20) + diff-lcs (1.5.0) + digest-crc (0.6.4) + rake (>= 12.0.0, < 14.0.0) docile (1.3.5) erubi (1.10.0) eventmachine (1.2.7) - exception_notification (4.4.3) - actionmailer (>= 4.0, < 7) - activesupport (>= 4.0, < 7) + exception_notification (4.5.0) + actionmailer (>= 5.2, < 8) + activesupport (>= 5.2, < 8) execjs (2.7.0) factory_bot (6.2.0) activesupport (>= 5.0.0) @@ -157,70 +190,119 @@ GEM railties (>= 5.0.0) fancybox-rails (0.3.1) railties (>= 3.1.0) - fast_gettext (2.1.0) - ffi (1.15.3) + faraday (0.17.5) + multipart-post (>= 1.2, < 3) + faraday_middleware (0.14.0) + faraday (>= 0.7.4, < 1.0) + fast_gettext (2.2.0) + ffi (1.15.5) fivemat (1.3.7) flipper (0.22.1) flipper-active_record (0.22.1) activerecord (>= 4.2, < 7) flipper (~> 0.22.1) + forwardable (1.3.2) gender_detector (2.0.0) - gettext (3.4.1) + gettext (3.4.3) + erubi locale (>= 2.0.5) + prime text (>= 1.3.0) gettext_i18n_rails (1.8.1) fast_gettext (>= 0.9.0) - globalid (0.5.2) + globalid (1.0.0) activesupport (>= 5.0) - globalize (5.3.1) - activemodel (>= 4.2, < 6.1) - activerecord (>= 4.2, < 6.1) + globalize (6.2.1) + activemodel (>= 4.2, < 7.1) + activerecord (>= 4.2, < 7.1) request_store (~> 1.0) gnuplot (2.6.2) + google-apis-core (0.4.2) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.10.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-storage_v1 (0.13.0) + google-apis-core (>= 0.4, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.2.0) + google-cloud-storage (1.36.2) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.1) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.1.3) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) hashdiff (1.0.1) highline (2.0.0) hodel_3000_compliant_logger (0.1.1) - holidays (8.4.1) + holidays (8.5.0) htmlentities (4.3.4) - i18n (1.8.11) + httpclient (2.8.3) + i18n (1.10.0) concurrent-ruby (~> 1.0) icalendar (2.7.1) ice_cube (~> 0.16) ice_cube (0.16.3) iso_country_codes (0.7.8) - jquery-rails (4.4.0) + jmespath (1.6.1) + jquery-rails (4.5.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-ui-rails (6.0.1) railties (>= 3.2.16) - json (2.6.1) + json (2.6.2) + jwt (2.3.0) launchy (2.4.3) addressable (~> 2.3) - libv8-node (15.14.0.1) - listen (3.7.0) + libv8-node (16.10.0.0) + libv8-node (16.10.0.0-aarch64-linux) + libv8-node (16.10.0.0-x86_64-linux) + listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) locale (2.1.3) - loofah (2.12.0) + loofah (2.18.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mahoro (0.5) mail (2.7.1) mini_mime (>= 0.1.1) - marcel (1.0.1) + marcel (1.0.2) + matrix (0.4.2) maxmind-db (1.0.0) + memoist (0.16.2) method_source (1.0.0) mime-types (2.99.3) mini_magick (4.11.0) - mini_mime (1.1.1) - mini_portile2 (2.6.1) - mini_racer (0.4.0) - libv8-node (~> 15.14.0.0) - minitest (5.14.4) + mini_mime (1.1.2) + mini_portile2 (2.8.0) + mini_racer (0.6.2) + libv8-node (~> 16.10.0.0) + minitest (5.16.1) money (6.16.0) i18n (>= 0.6.4, <= 2) - multi_json (1.13.1) + multi_json (1.15.0) + multipart-post (2.1.1) net-scp (1.2.1) net-ssh (>= 2.6.5) net-sftp (2.1.2) @@ -229,44 +311,52 @@ GEM net-ssh-gateway (2.0.0) net-ssh (>= 4.0.0) nio4r (2.5.8) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) + nokogiri (1.13.6) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) + nokogiri (1.13.6-aarch64-linux) + racc (~> 1.4) + nokogiri (1.13.6-x86_64-linux) racc (~> 1.4) oink (0.10.1) activerecord hodel_3000_compliant_logger open4 (1.3.4) - parallel (1.21.0) - parser (3.0.2.0) + os (1.1.4) + parallel (1.22.1) + parser (3.1.2.0) ast (~> 2.4.1) - pg (1.2.3) + pg (1.4.1) + prime (0.1.2) + forwardable + singleton pry (0.13.1) coderay (~> 1.1) method_source (~> 1.0) pry-byebug (3.9.0) byebug (~> 11.0) pry (~> 0.13.0) - public_suffix (4.0.6) - racc (1.5.2) - rack (2.2.3) + public_suffix (4.0.7) + racc (1.6.0) + rack (2.2.3.1) rack-test (1.1.0) rack (>= 1.0, < 3) rack-utf8_sanitizer (1.7.0) rack (>= 1.0, < 3.0) - rails (6.0.4.1) - actioncable (= 6.0.4.1) - actionmailbox (= 6.0.4.1) - actionmailer (= 6.0.4.1) - actionpack (= 6.0.4.1) - actiontext (= 6.0.4.1) - actionview (= 6.0.4.1) - activejob (= 6.0.4.1) - activemodel (= 6.0.4.1) - activerecord (= 6.0.4.1) - activestorage (= 6.0.4.1) - activesupport (= 6.0.4.1) - bundler (>= 1.3.0) - railties (= 6.0.4.1) + rails (6.1.6) + actioncable (= 6.1.6) + actionmailbox (= 6.1.6) + actionmailer (= 6.1.6) + actionpack (= 6.1.6) + actiontext (= 6.1.6) + actionview (= 6.1.6) + activejob (= 6.1.6) + activemodel (= 6.1.6) + activerecord (= 6.1.6) + activestorage (= 6.1.6) + activesupport (= 6.1.6) + bundler (>= 1.15.0) + railties (= 6.1.6) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -277,44 +367,49 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.4.2) loofah (~> 2.3) - rails-i18n (6.0.0) + rails-i18n (7.0.3) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 7) - railties (6.0.4.1) - actionpack (= 6.0.4.1) - activesupport (= 6.0.4.1) + railties (>= 6.0.0, < 8) + railties (6.1.6) + actionpack (= 6.1.6) + activesupport (= 6.1.6) method_source - rake (>= 0.8.7) - thor (>= 0.20.3, < 2.0) - rainbow (3.0.0) + rake (>= 12.2) + thor (~> 1.0) + rainbow (3.1.1) rake (13.0.6) rb-fsevent (0.11.0) rb-inotify (0.10.1) ffi (~> 1.0) - recaptcha (5.8.1) + recaptcha (5.10.0) json - regexp_parser (2.1.1) - request_store (1.5.0) + regexp_parser (2.5.0) + representable (3.1.1) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + request_store (1.5.1) rack (>= 1.4) + retriable (3.1.2) rexml (3.2.5) rolify (5.3.0) rotp (6.2.0) - routing-filter (0.6.3) - actionpack (>= 4.2) - activesupport (>= 4.2) + routing-filter (0.7.0) + actionpack (>= 6.1) + activesupport (>= 6.1) rspec-activemodel-mocks (1.1.0) activemodel (>= 3.0) activesupport (>= 3.0) rspec-mocks (>= 2.99, < 4.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + rspec-core (3.11.0) + rspec-support (~> 3.11.0) + rspec-expectations (3.11.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) + rspec-support (~> 3.11.0) + rspec-mocks (3.11.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (5.0.2) + rspec-support (~> 3.11.0) + rspec-rails (5.1.2) actionpack (>= 5.2) activesupport (>= 5.2) railties (>= 5.2) @@ -322,22 +417,22 @@ GEM rspec-expectations (~> 3.10) rspec-mocks (~> 3.10) rspec-support (~> 3.10) - rspec-support (3.10.2) - rubocop (1.22.3) + rspec-support (3.11.0) + rubocop (1.30.1) parallel (~> 1.10) - parser (>= 3.0.0.0) + parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.12.0, < 2.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.18.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.13.0) - parser (>= 3.0.1.1) - rubocop-performance (1.12.0) + rubocop-ast (1.18.0) + parser (>= 3.1.1.0) + rubocop-performance (1.14.2) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.12.4) + rubocop-rails (2.15.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.7.0, < 2.0) @@ -352,40 +447,47 @@ GEM sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) secure_headers (6.3.3) + signet (0.16.1) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.0) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) simplecov (0.17.1) docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) simplecov-lcov (0.7.0) + singleton (0.1.1) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) sprockets (>= 3.0.0) statistics2 (0.54) - stripe (5.39.0) + stripe (5.55.0) syslog_protocol (0.9.2) text (1.3.1) thin (1.8.1) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) - thor (1.1.0) - thread_safe (0.3.6) + thor (1.2.1) tilt (2.0.10) - tzinfo (1.2.9) - thread_safe (~> 0.1) + trailblazer-option (0.1.2) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + uber (0.1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) unicode (0.4.4.4) - unicode-display_width (2.1.0) + unicode-display_width (2.2.0) unidecoder (1.1.2) - uniform_notifier (1.14.2) + uniform_notifier (1.16.0) vpim (13.11.11) - web-console (4.1.0) + web-console (4.2.0) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) @@ -394,6 +496,7 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) + webrick (1.7.0) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -403,72 +506,77 @@ GEM rexml xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.5.1) + zeitwerk (2.6.0) zip_tricks (5.6.0) PLATFORMS + aarch64-linux ruby + x86_64-linux DEPENDENCIES active_model_otp acts_as_versioned! alaveteli_features! - annotate (< 3.1.1) - bcrypt (~> 3.1.16) + annotate (< 3.2.1) + aws-sdk-s3 + azure-storage + bcrypt (~> 3.1.18) bootstrap-sass (~> 2.3.2.2) - bullet (~> 6.1.5) - cancancan (~> 3.3.0) + bullet (~> 7.0.2) + cancancan (~> 3.4.0) capistrano (~> 2.15.0, < 3.0.0) - capybara (~> 3.35.3) + capybara (~> 3.37.1) charlock_holmes (~> 0.7.7) - dalli (~> 3.0.4) - exception_notification (~> 4.4.3) + dalli (~> 3.2.2) + exception_notification (~> 4.5.0) factory_bot_rails (~> 6.2.0) fancybox-rails (~> 0.3.0) - fast_gettext (~> 2.1.0) + fast_gettext (~> 2.2.0) fivemat (~> 1.3.7) gender_detector (~> 2.0.0) - gettext (~> 3.4.1) + gettext (~> 3.4.3) gettext_i18n_rails (~> 1.8.1) - globalize (~> 5.3.0) + globalize (~> 6.2.1) gnuplot (~> 2.6.0) - holidays (~> 8.4.1) + google-cloud-storage (~> 1.36) + holidays (~> 8.5.0) htmlentities (~> 4.3.0) - i18n (~> 1.8.11) + i18n (~> 1.10.0) icalendar (~> 2.7.1) iso_country_codes (~> 0.7.8) - jquery-rails (~> 4.4.0) + jquery-rails (~> 4.5.0) jquery-ui-rails (~> 6.0.0) - json (~> 2.6.1) + json (~> 2.6.2) launchy (< 2.5.0) - listen (>= 3.0.5, < 3.7.1) + listen (>= 3.0.5, < 3.7.2) locale (~> 2.1.3) mahoro (~> 0.5) mail (~> 2.7.1) maxmind-db (~> 1.0.0) mime-types (< 3.0.0) mini_magick (~> 4.11.0) - mini_racer (~> 0.4.0) + mini_racer (~> 0.6.2) money (~> 6.16.0) net-ssh (~> 6.1.0) net-ssh-gateway (>= 1.1.0, < 3.0.0) - nokogiri (~> 1.12.5) + nokogiri (~> 1.13.6) oink (~> 0.10.1) open4 (~> 1.3.0) - pg (~> 1.2.3) + pg (~> 1.4.1) pry (~> 0.13.0) pry-byebug (~> 3.9.0) rack (~> 2.2.3) rack-utf8_sanitizer (~> 1.7.0) - rails (~> 6.0.3) + rails (~> 6.1.6) rails-controller-testing - rails-i18n (~> 6.0.0) - recaptcha (~> 5.8.1) + rails-i18n (~> 7.0.3) + recaptcha (~> 5.10.0) rolify (~> 5.3.0) - routing-filter (~> 0.6.2) + routing-filter (~> 0.7.0) rspec-activemodel-mocks (~> 1.1.0) - rspec-rails (~> 5.0.2) - rubocop (~> 1.22.3) + rspec-rails (~> 5.1.2) + rubocop (~> 1.30.1) rubocop-performance rubocop-rails ruby-msg (~> 1.5.0)! @@ -479,7 +587,7 @@ DEPENDENCIES simplecov-lcov (~> 0.7.0) statistics2 (~> 0.54) strip_attributes! - stripe (~> 5.39.0) + stripe (~> 5.55.0) stripe-ruby-mock! syslog_protocol (~> 0.9.0) thin (~> 1.8.1) diff --git a/Gemfile.rails_next.lock b/Gemfile.rails_next.lock index 75c402a0b6..9c98803de2 100644 --- a/Gemfile.rails_next.lock +++ b/Gemfile.rails_next.lock @@ -9,11 +9,11 @@ GIT GIT remote: https://github.com/mysociety/strip_attributes.git - revision: 62a5e1ee26501ad4c111b855cd73a5653091300b - branch: globalize3-rails5.2 + revision: 842a889258a897692296dff8445bb9dc12e676f8 + branch: globalize3-rails7 specs: - strip_attributes (1.11.0) - activemodel (>= 3.0, < 7.0) + strip_attributes (1.12.0) + activemodel (>= 3.0, < 8.0) GIT remote: https://github.com/stripe-ruby-mock/stripe-ruby-mock @@ -40,45 +40,52 @@ PATH flipper (~> 0.10) flipper-active_record (~> 0.10) mime-types (< 3.0.0) - rails (~> 6.1.4) + rails (~> 7.0.2) GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.1) - actionpack (= 6.1.4.1) - activesupport (= 6.1.4.1) + actioncable (7.0.3) + actionpack (= 7.0.3) + activesupport (= 7.0.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.1) - actionpack (= 6.1.4.1) - activejob (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + actionmailbox (7.0.3) + actionpack (= 7.0.3) + activejob (= 7.0.3) + activerecord (= 7.0.3) + activestorage (= 7.0.3) + activesupport (= 7.0.3) mail (>= 2.7.1) - actionmailer (6.1.4.1) - actionpack (= 6.1.4.1) - actionview (= 6.1.4.1) - activejob (= 6.1.4.1) - activesupport (= 6.1.4.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.3) + actionpack (= 7.0.3) + actionview (= 7.0.3) + activejob (= 7.0.3) + activesupport (= 7.0.3) mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp rails-dom-testing (~> 2.0) - actionpack (6.1.4.1) - actionview (= 6.1.4.1) - activesupport (= 6.1.4.1) - rack (~> 2.0, >= 2.0.9) + actionpack (7.0.3) + actionview (= 7.0.3) + activesupport (= 7.0.3) + rack (~> 2.0, >= 2.2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.1) - actionpack (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + actiontext (7.0.3) + actionpack (= 7.0.3) + activerecord (= 7.0.3) + activestorage (= 7.0.3) + activesupport (= 7.0.3) + globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (6.1.4.1) - activesupport (= 6.1.4.1) + actionview (7.0.3) + activesupport (= 7.0.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -86,51 +93,76 @@ GEM active_model_otp (2.3.1) activemodel rotp (~> 6.2.0) - activejob (6.1.4.1) - activesupport (= 6.1.4.1) + activejob (7.0.3) + activesupport (= 7.0.3) globalid (>= 0.3.6) - activemodel (6.1.4.1) - activesupport (= 6.1.4.1) - activerecord (6.1.4.1) - activemodel (= 6.1.4.1) - activesupport (= 6.1.4.1) - activestorage (6.1.4.1) - actionpack (= 6.1.4.1) - activejob (= 6.1.4.1) - activerecord (= 6.1.4.1) - activesupport (= 6.1.4.1) - marcel (~> 1.0.0) + activemodel (7.0.3) + activesupport (= 7.0.3) + activerecord (7.0.3) + activemodel (= 7.0.3) + activesupport (= 7.0.3) + activestorage (7.0.3) + actionpack (= 7.0.3) + activejob (= 7.0.3) + activerecord (= 7.0.3) + activesupport (= 7.0.3) + marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.1) + activesupport (7.0.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) - zeitwerk (~> 2.3) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) - annotate (3.1.0) - activerecord (>= 3.2, < 7.0) + annotate (3.2.0) + activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) - bcrypt (3.1.16) + aws-eventstream (1.2.0) + aws-partitions (1.590.0) + aws-sdk-core (3.131.1) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.525.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.57.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.114.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.5.0) + aws-eventstream (~> 1, >= 1.0.2) + azure-core (0.1.15) + faraday (~> 0.9) + faraday_middleware (~> 0.10) + nokogiri (~> 1.6) + azure-storage (0.15.0.preview) + azure-core (~> 0.1) + faraday (~> 0.9) + faraday_middleware (~> 0.10) + nokogiri (~> 1.6, >= 1.6.8) + bcrypt (3.1.18) bindex (0.8.1) bootstrap-sass (2.3.2.2) sass (~> 3.2) builder (3.2.4) - bullet (6.1.5) + bullet (7.0.2) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) byebug (11.1.3) - cancancan (3.3.0) + cancancan (3.4.0) capistrano (2.15.9) highline net-scp (>= 1.0.0) net-sftp (>= 2.0.0) net-ssh (>= 2.0.14) net-ssh-gateway (>= 1.1.0) - capybara (3.35.3) + capybara (3.37.1) addressable + matrix mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) @@ -139,20 +171,24 @@ GEM xpath (~> 3.2) charlock_holmes (0.7.7) coderay (1.1.3) - concurrent-ruby (1.1.9) + concurrent-ruby (1.1.10) crack (0.4.5) rexml crass (1.0.6) daemons (1.4.0) - dalli (3.0.4) + dalli (3.2.2) dante (0.2.0) - diff-lcs (1.4.4) + declarative (0.0.20) + diff-lcs (1.5.0) + digest (3.1.0) + digest-crc (0.6.4) + rake (>= 12.0.0, < 14.0.0) docile (1.3.5) erubi (1.10.0) eventmachine (1.2.7) - exception_notification (4.4.3) - actionmailer (>= 4.0, < 7) - activesupport (>= 4.0, < 7) + exception_notification (4.5.0) + actionmailer (>= 5.2, < 8) + activesupport (>= 5.2, < 8) execjs (2.7.0) factory_bot (6.2.0) activesupport (>= 5.0.0) @@ -161,117 +197,190 @@ GEM railties (>= 5.0.0) fancybox-rails (0.3.1) railties (>= 3.1.0) - fast_gettext (2.1.0) - ffi (1.15.3) + faraday (0.17.5) + multipart-post (>= 1.2, < 3) + faraday_middleware (0.14.0) + faraday (>= 0.7.4, < 1.0) + fast_gettext (2.2.0) + ffi (1.15.5) fivemat (1.3.7) - flipper (0.22.1) - flipper-active_record (0.22.1) - activerecord (>= 4.2, < 7) - flipper (~> 0.22.1) + flipper (0.24.1) + flipper-active_record (0.24.1) + activerecord (>= 4.2, < 8) + flipper (~> 0.24.1) + forwardable (1.3.2) gender_detector (2.0.0) - gettext (3.4.1) + gettext (3.4.3) + erubi locale (>= 2.0.5) + prime text (>= 1.3.0) gettext_i18n_rails (1.8.1) fast_gettext (>= 0.9.0) - globalid (0.5.2) + globalid (1.0.0) activesupport (>= 5.0) - globalize (6.0.1) - activemodel (>= 4.2, < 7.0) - activerecord (>= 4.2, < 7.0) + globalize (6.2.1) + activemodel (>= 4.2, < 7.1) + activerecord (>= 4.2, < 7.1) request_store (~> 1.0) gnuplot (2.6.2) + google-apis-core (0.4.2) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.10.0) + google-apis-core (>= 0.4, < 2.a) + google-apis-storage_v1 (0.13.0) + google-apis-core (>= 0.4, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.2.0) + google-cloud-storage (1.36.2) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.1) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.1.3) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) hashdiff (1.0.1) highline (2.0.0) hodel_3000_compliant_logger (0.1.1) - holidays (8.4.1) + holidays (8.5.0) htmlentities (4.3.4) - i18n (1.8.11) + httpclient (2.8.3) + i18n (1.10.0) concurrent-ruby (~> 1.0) icalendar (2.7.1) ice_cube (~> 0.16) ice_cube (0.16.3) iso_country_codes (0.7.8) - jquery-rails (4.4.0) + jmespath (1.6.1) + jquery-rails (4.5.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-ui-rails (6.0.1) railties (>= 3.2.16) - json (2.6.1) + json (2.6.2) + jwt (2.3.0) launchy (2.4.3) addressable (~> 2.3) - libv8-node (15.14.0.1) - listen (3.7.0) + libv8-node (16.10.0.0) + libv8-node (16.10.0.0-aarch64-linux) + libv8-node (16.10.0.0-x86_64-linux) + listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) locale (2.1.3) - loofah (2.12.0) + loofah (2.18.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mahoro (0.5) mail (2.7.1) mini_mime (>= 0.1.1) - marcel (1.0.1) + marcel (1.0.2) + matrix (0.4.2) maxmind-db (1.0.0) + memoist (0.16.2) method_source (1.0.0) mime-types (2.99.3) mini_magick (4.11.0) - mini_mime (1.1.1) - mini_portile2 (2.6.1) - mini_racer (0.4.0) - libv8-node (~> 15.14.0.0) - minitest (5.14.4) + mini_mime (1.1.2) + mini_portile2 (2.8.0) + mini_racer (0.6.2) + libv8-node (~> 16.10.0.0) + minitest (5.16.1) money (6.16.0) i18n (>= 0.6.4, <= 2) - multi_json (1.13.1) + multi_json (1.15.0) + multipart-post (2.1.1) + net-http (0.1.1) + net-protocol + uri + net-imap (0.2.3) + digest + net-protocol + strscan + net-pop (0.1.1) + digest + net-protocol + timeout + net-protocol (0.1.3) + timeout net-scp (1.2.1) net-ssh (>= 2.6.5) net-sftp (2.1.2) net-ssh (>= 2.6.5) + net-smtp (0.3.1) + digest + net-protocol + timeout net-ssh (6.1.0) net-ssh-gateway (2.0.0) net-ssh (>= 4.0.0) nio4r (2.5.8) - nokogiri (1.12.5) - mini_portile2 (~> 2.6.1) + nokogiri (1.13.6) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) + nokogiri (1.13.6-aarch64-linux) + racc (~> 1.4) + nokogiri (1.13.6-x86_64-linux) racc (~> 1.4) oink (0.10.1) activerecord hodel_3000_compliant_logger open4 (1.3.4) - parallel (1.21.0) - parser (3.0.2.0) + os (1.1.4) + parallel (1.22.1) + parser (3.1.2.0) ast (~> 2.4.1) - pg (1.2.3) + pg (1.4.1) + prime (0.1.2) + forwardable + singleton pry (0.13.1) coderay (~> 1.1) method_source (~> 1.0) pry-byebug (3.9.0) byebug (~> 11.0) pry (~> 0.13.0) - public_suffix (4.0.6) - racc (1.5.2) - rack (2.2.3) + public_suffix (4.0.7) + racc (1.6.0) + rack (2.2.3.1) rack-test (1.1.0) rack (>= 1.0, < 3) rack-utf8_sanitizer (1.7.0) rack (>= 1.0, < 3.0) - rails (6.1.4.1) - actioncable (= 6.1.4.1) - actionmailbox (= 6.1.4.1) - actionmailer (= 6.1.4.1) - actionpack (= 6.1.4.1) - actiontext (= 6.1.4.1) - actionview (= 6.1.4.1) - activejob (= 6.1.4.1) - activemodel (= 6.1.4.1) - activerecord (= 6.1.4.1) - activestorage (= 6.1.4.1) - activesupport (= 6.1.4.1) + rails (7.0.3) + actioncable (= 7.0.3) + actionmailbox (= 7.0.3) + actionmailer (= 7.0.3) + actionpack (= 7.0.3) + actiontext (= 7.0.3) + actionview (= 7.0.3) + activejob (= 7.0.3) + activemodel (= 7.0.3) + activerecord (= 7.0.3) + activestorage (= 7.0.3) + activesupport (= 7.0.3) bundler (>= 1.15.0) - railties (= 6.1.4.1) - sprockets-rails (>= 2.0.0) + railties (= 7.0.3) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -281,25 +390,31 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.4.2) loofah (~> 2.3) - rails-i18n (6.0.0) + rails-i18n (7.0.3) i18n (>= 0.7, < 2) - railties (>= 6.0.0, < 7) - railties (6.1.4.1) - actionpack (= 6.1.4.1) - activesupport (= 6.1.4.1) + railties (>= 6.0.0, < 8) + railties (7.0.3) + actionpack (= 7.0.3) + activesupport (= 7.0.3) method_source - rake (>= 0.13) + rake (>= 12.2) thor (~> 1.0) - rainbow (3.0.0) + zeitwerk (~> 2.5) + rainbow (3.1.1) rake (13.0.6) rb-fsevent (0.11.0) rb-inotify (0.10.1) ffi (~> 1.0) - recaptcha (5.8.1) + recaptcha (5.10.0) json - regexp_parser (2.1.1) - request_store (1.5.0) + regexp_parser (2.5.0) + representable (3.1.1) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + request_store (1.5.1) rack (>= 1.4) + retriable (3.1.2) rexml (3.2.5) rolify (5.3.0) rotp (6.2.0) @@ -310,15 +425,15 @@ GEM activemodel (>= 3.0) activesupport (>= 3.0) rspec-mocks (>= 2.99, < 4.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + rspec-core (3.11.0) + rspec-support (~> 3.11.0) + rspec-expectations (3.11.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) + rspec-support (~> 3.11.0) + rspec-mocks (3.11.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (5.0.2) + rspec-support (~> 3.11.0) + rspec-rails (5.1.2) actionpack (>= 5.2) activesupport (>= 5.2) railties (>= 5.2) @@ -326,22 +441,22 @@ GEM rspec-expectations (~> 3.10) rspec-mocks (~> 3.10) rspec-support (~> 3.10) - rspec-support (3.10.2) - rubocop (1.22.3) + rspec-support (3.11.0) + rubocop (1.30.1) parallel (~> 1.10) - parser (>= 3.0.0.0) + parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.12.0, < 2.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.18.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.13.0) - parser (>= 3.0.1.1) - rubocop-performance (1.12.0) + rubocop-ast (1.18.0) + parser (>= 3.1.1.0) + rubocop-performance (1.14.2) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.12.4) + rubocop-rails (2.15.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.7.0, < 2.0) @@ -356,39 +471,50 @@ GEM sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) secure_headers (6.3.3) + signet (0.16.1) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.0) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) simplecov (0.17.1) docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) simplecov-lcov (0.7.0) + singleton (0.1.1) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) sprockets (>= 3.0.0) statistics2 (0.54) - stripe (5.39.0) + stripe (5.55.0) + strscan (3.0.3) syslog_protocol (0.9.2) text (1.3.1) thin (1.8.1) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) - thor (1.1.0) + thor (1.2.1) tilt (2.0.10) + timeout (0.2.0) + trailblazer-option (0.1.2) tzinfo (2.0.4) concurrent-ruby (~> 1.0) + uber (0.1.0) uglifier (4.2.0) execjs (>= 0.3.0, < 3) unicode (0.4.4.4) - unicode-display_width (2.1.0) + unicode-display_width (2.2.0) unidecoder (1.1.2) - uniform_notifier (1.14.2) + uniform_notifier (1.16.0) + uri (0.10.0) vpim (13.11.11) - web-console (4.1.0) + web-console (4.2.0) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) @@ -397,6 +523,7 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) + webrick (1.7.0) websocket-driver (0.7.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -406,72 +533,78 @@ GEM rexml xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.5.1) + zeitwerk (2.5.4) zip_tricks (5.6.0) PLATFORMS + aarch64-linux ruby + x86_64-linux DEPENDENCIES active_model_otp acts_as_versioned! alaveteli_features! - annotate (< 3.1.1) - bcrypt (~> 3.1.16) + annotate (< 3.2.1) + aws-sdk-s3 + azure-storage + bcrypt (~> 3.1.18) bootstrap-sass (~> 2.3.2.2) - bullet (~> 6.1.5) - cancancan (~> 3.3.0) + bullet (~> 7.0.2) + cancancan (~> 3.4.0) capistrano (~> 2.15.0, < 3.0.0) - capybara (~> 3.35.3) + capybara (~> 3.37.1) charlock_holmes (~> 0.7.7) - dalli (~> 3.0.4) - exception_notification (~> 4.4.3) + dalli (~> 3.2.2) + exception_notification (~> 4.5.0) factory_bot_rails (~> 6.2.0) fancybox-rails (~> 0.3.0) - fast_gettext (~> 2.1.0) + fast_gettext (~> 2.2.0) fivemat (~> 1.3.7) gender_detector (~> 2.0.0) - gettext (~> 3.4.1) + gettext (~> 3.4.3) gettext_i18n_rails (~> 1.8.1) - globalize (~> 6.0.0) + globalize (~> 6.2.1) gnuplot (~> 2.6.0) - holidays (~> 8.4.1) + google-cloud-storage (~> 1.36) + holidays (~> 8.5.0) htmlentities (~> 4.3.0) - i18n (~> 1.8.11) + i18n (~> 1.10.0) icalendar (~> 2.7.1) iso_country_codes (~> 0.7.8) - jquery-rails (~> 4.4.0) + jquery-rails (~> 4.5.0) jquery-ui-rails (~> 6.0.0) - json (~> 2.6.1) + json (~> 2.6.2) launchy (< 2.5.0) - listen (>= 3.0.5, < 3.7.1) + listen (>= 3.0.5, < 3.7.2) locale (~> 2.1.3) mahoro (~> 0.5) mail (~> 2.7.1) maxmind-db (~> 1.0.0) mime-types (< 3.0.0) mini_magick (~> 4.11.0) - mini_racer (~> 0.4.0) + mini_racer (~> 0.6.2) money (~> 6.16.0) + net-http (= 0.1.1) net-ssh (~> 6.1.0) net-ssh-gateway (>= 1.1.0, < 3.0.0) - nokogiri (~> 1.12.5) + nokogiri (~> 1.13.6) oink (~> 0.10.1) open4 (~> 1.3.0) - pg (~> 1.2.3) + pg (~> 1.4.1) pry (~> 0.13.0) pry-byebug (~> 3.9.0) rack (~> 2.2.3) rack-utf8_sanitizer (~> 1.7.0) - rails (~> 6.1.4) + rails (~> 7.0.3) rails-controller-testing - rails-i18n (~> 6.0.0) - recaptcha (~> 5.8.1) + rails-i18n (~> 7.0.3) + recaptcha (~> 5.10.0) rolify (~> 5.3.0) routing-filter (~> 0.7.0) rspec-activemodel-mocks (~> 1.1.0) - rspec-rails (~> 5.0.2) - rubocop (~> 1.22.3) + rspec-rails (~> 5.1.2) + rubocop (~> 1.30.1) rubocop-performance rubocop-rails ruby-msg (~> 1.5.0)! @@ -482,13 +615,14 @@ DEPENDENCIES simplecov-lcov (~> 0.7.0) statistics2 (~> 0.54) strip_attributes! - stripe (~> 5.39.0) + stripe (~> 5.55.0) stripe-ruby-mock! syslog_protocol (~> 0.9.0) thin (~> 1.8.1) uglifier (~> 4.2.0) unicode (~> 0.4.4) unidecoder (~> 1.1.0) + uri (= 0.10.0) vpim (~> 13.11.11) web-console (>= 3.3.0) webmock (~> 3.14.0) diff --git a/README.md b/README.md index ab3a9806a6..9fe4a3ecbd 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,9 @@ see [the project website](http://alaveteli.org) for instructions on installing A Every Alaveteli commit is tested by GitHub Actions on the [following Ruby platforms](https://github.com/mysociety/alaveteli/blob/develop/.github/workflows/ci.yml#L15) -* ruby-2.5 -* ruby-2.6 * ruby-2.7 -If you use a ruby version management tool (such as RVM or .rbenv) and want to use the default development version used by the Alaveteli team (currently 2.5.8), you can create a `.ruby-version` symlink with a target of `.ruby-version.example` to switch to that automatically in the project directory. +If you use a ruby version management tool (such as RVM or .rbenv) and want to use the default development version used by the Alaveteli team (currently 2.7.4), you can create a `.ruby-version` symlink with a target of `.ruby-version.example` to switch to that automatically in the project directory. ## How to contribute diff --git a/Vagrantfile b/Vagrantfile index 02b04fe5ec..401d8ec794 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -102,7 +102,7 @@ DEFAULTS = { 'public_network' => false, 'memory' => 1536, 'themes_dir' => '../alaveteli-themes', - 'os' => 'stretch64', + 'os' => 'bullseye64', 'name' => 'default', 'use_nfs' => false, 'show_settings' => false, @@ -124,22 +124,10 @@ else end SUPPORTED_OPERATING_SYSTEMS = { - 'bionic64' => { - box: 'ubuntu/bionic64', - box_url: 'https://app.vagrantup.com/ubuntu/boxes/bionic64' - }, 'focal64' => { box: 'ubuntu/focal64', box_url: 'https://app.vagrantup.com/ubuntu/boxes/focal64' }, - 'stretch64' => { - box: 'debian/stretch64', - box_url: 'https://app.vagrantup.com/debian/boxes/stretch64' - }, - 'buster64' => { - box: 'debian/buster64', - box_url: 'https://app.vagrantup.com/debian/boxes/buster64' - }, 'bullseye64' => { box: 'debian/bullseye64', box_url: 'https://app.vagrantup.com/debian/boxes/bullseye64' diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index cb13379c27..00eaf95da2 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -19,6 +19,7 @@ //= link new-request.js //= link time_series.js //= link admin.css +//= link admin/print.css //= link bootstrap-dropdown.js //= link widget.css //= link request-attachments.js diff --git a/app/assets/images/act-links-sprite.png b/app/assets/images/act-links-sprite.png index 3fefed8f6a..171a399f6d 100644 Binary files a/app/assets/images/act-links-sprite.png and b/app/assets/images/act-links-sprite.png differ diff --git a/app/assets/images/act-links-sprite@2.png b/app/assets/images/act-links-sprite@2.png index b2937813ee..3fc83bbf96 100644 Binary files a/app/assets/images/act-links-sprite@2.png and b/app/assets/images/act-links-sprite@2.png differ diff --git a/app/assets/javascripts/admin/admin.js b/app/assets/javascripts/admin/admin.js index b5fc50ad3a..afadf45677 100644 --- a/app/assets/javascripts/admin/admin.js +++ b/app/assets/javascripts/admin/admin.js @@ -2,10 +2,10 @@ jQuery(function() { $('.locales a:first').tab('show'); $('.accordion-body').on('hidden', function() { - return $(this).prev().find('i').first().removeClass().addClass('icon-chevron-right'); + return $(this).prev().find('i.icon-chevron-down').first().removeClass().addClass('icon-chevron-right'); }); $('.accordion-body').on('shown', function() { - return $(this).prev().find('i').first().removeClass().addClass('icon-chevron-down'); + return $(this).prev().find('i.icon-chevron-right').first().removeClass().addClass('icon-chevron-down'); }); $('.toggle-hidden').on('click', function() { $(this).parents('td').find('div:hidden').show(); @@ -90,3 +90,30 @@ }); }).call(this); + +$(function() { + $('.select_all').click(function (event) { + var selectables = $("input[name='" + $(this).data('target') + "']"); + var state = $(this).data('state'); + + if (state == "unchecked") { + selectables.each(function () { + $(this).prop('checked', true); + }); + + $(this).data('state', 'checked'); + $(this).text("Deselect all"); + return false; + } else { + selectables.each(function () { + $(this).prop('checked', false); + }); + + $(this).data('state', 'unchecked'); + $(this).text("Select all"); + return false; + } + }); + + return false; +}); diff --git a/app/assets/javascripts/alaveteli_pro/alaveteli_pro.js b/app/assets/javascripts/alaveteli_pro/alaveteli_pro.js index cfc0e5980a..8bd641d40c 100644 --- a/app/assets/javascripts/alaveteli_pro/alaveteli_pro.js +++ b/app/assets/javascripts/alaveteli_pro/alaveteli_pro.js @@ -4,6 +4,7 @@ //= require alaveteli_pro/embargo_dropdown //= require alaveteli_pro/status_dropdown //= require alaveteli_pro/marketing +//= require alaveteli_pro/existing_batch // These modules must be initialised first, because later sub components may // need access to things defined in either one of them. @@ -22,5 +23,6 @@ //= require alaveteli_pro/batch_authority_search/results //= require alaveteli_pro/batch_authority_search/pagination //= require alaveteli_pro/batch_authority_search/result +//= require alaveteli_pro/batch_authority_search/count //= require alaveteli_pro/request_navigation \ No newline at end of file diff --git a/app/assets/javascripts/alaveteli_pro/batch_authority_search/browse.js b/app/assets/javascripts/alaveteli_pro/batch_authority_search/browse.js index 3e69f34bc9..0caacaa52f 100644 --- a/app/assets/javascripts/alaveteli_pro/batch_authority_search/browse.js +++ b/app/assets/javascripts/alaveteli_pro/batch_authority_search/browse.js @@ -22,7 +22,7 @@ var fetchBodies = function fetchBodies(url, group) { toggleSpinner(group); $.ajax({ - url: url, + url: DraftBatchSummary.urlWithDraftID(url), dataType: 'html', success: function (data) { group.append(data); @@ -30,6 +30,7 @@ toggleSpinner(group); $draft.trigger(DraftEvents.bodyAdded); $search.trigger(SearchEvents.domUpdated); + $search.trigger(SearchEvents.rendered); } }); } @@ -60,8 +61,6 @@ $search = BatchAuthoritySearch.$el; $draft = DraftBatchSummary.$el; - $search.on(SearchEvents.rendered, bindListItemAnchors); - collapseTopLevelGroups(); bindListItemAnchors(); }); diff --git a/app/assets/javascripts/alaveteli_pro/batch_authority_search/count.js b/app/assets/javascripts/alaveteli_pro/batch_authority_search/count.js new file mode 100644 index 0000000000..7ea83428b2 --- /dev/null +++ b/app/assets/javascripts/alaveteli_pro/batch_authority_search/count.js @@ -0,0 +1,47 @@ +// Handles updating the batch authority search authority count +(function($, BatchAuthoritySearch, DraftBatchSummary) { + var DraftEvents = DraftBatchSummary.Events; + + var $search, + $draft, + $count, + messageTemplateZero, + messageTemplateOne, + messageTemplateMany; + + // Update the count + var updateCount = function(e) { + count = publicBodiesCount(); + if (count == 0) { messageTemplate = messageTemplateZero; } + else if (count == 1) { messageTemplate = messageTemplateOne; } + else { messageTemplate = messageTemplateMany; } + + $count.text(messageTemplate.replace('{{count}}', count)); + }; + + // Return the number of public bodies added + var publicBodiesCount = function() { + return $( + '.js-draft-batch-request-summary .batch-builder__list__item', $draft + ).length; + } + + $(function() { + $search = BatchAuthoritySearch.$el; + $draft = DraftBatchSummary.$el; + $count = $('.batch-builder__actions__count', $search); + messageTemplateZero = $count.data('message-template-zero'); + messageTemplateOne = $count.data('message-template-one'); + messageTemplateMany = $count.data('message-template-many'); + + // not count element present, escape before binding events + if (!$count.get(0)) { return } + + updateCount(); + + $draft.on(DraftEvents.bodyAdded, updateCount); + $draft.on(DraftEvents.bodyRemoved, updateCount); + }); +})(window.jQuery, + window.AlaveteliPro.BatchAuthoritySearch, + window.AlaveteliPro.DraftBatchSummary); diff --git a/app/assets/javascripts/alaveteli_pro/batch_mode/mode-switcher.js b/app/assets/javascripts/alaveteli_pro/batch_mode/mode-switcher.js index aac2d54bd4..7093ad89a2 100644 --- a/app/assets/javascripts/alaveteli_pro/batch_mode/mode-switcher.js +++ b/app/assets/javascripts/alaveteli_pro/batch_mode/mode-switcher.js @@ -8,28 +8,7 @@ var $tabs = $batch.find('.tab-title'); $tabs.find('a').attr('href', function(i, href) { - // Parse the href so that we can modify the draft_id param - var urlParts = href.split('?'); - var path = urlParts[0]; - var querystring = urlParts[1]; - var params = $.deparam(querystring); - - // 1. There is a DraftBatchSummary.draftId, but there is no draft_id param - // in the href, so we want to add the param. - // - // 2. There is a DraftBatchSummary.draftId, and we have an existing - // draft_id param in the href, so we want to update it to make sure we're - // using the current DraftBatchSummary.draftId. - // - // 3. There is no DraftBatchSummary.draftId, but we have a draft_id param - // in the href, so we want to remove the draft_id param. - if (DraftBatchSummary.draftId) { - params.draft_id = DraftBatchSummary.draftId; - } else if (params.draft_id) { - delete params.draft_id; - } - - return path + '?' + $.param(params); + return DraftBatchSummary.urlWithDraftID(href); }); }; diff --git a/app/assets/javascripts/alaveteli_pro/draft_batch_summary/body-list.js b/app/assets/javascripts/alaveteli_pro/draft_batch_summary/body-list.js index 89c7b15186..82f03129e2 100644 --- a/app/assets/javascripts/alaveteli_pro/draft_batch_summary/body-list.js +++ b/app/assets/javascripts/alaveteli_pro/draft_batch_summary/body-list.js @@ -34,6 +34,11 @@ DraftBatchSummary.draftId = $(summarySelector, $draft).data('draft-id'); if (previousDraftId != DraftBatchSummary.draftId) { $('.js-draft-id').val(DraftBatchSummary.draftId); + + // update address bar with new draft ID + url = DraftBatchSummary.urlWithDraftID(window.location.href); + window.history.pushState({}, '', url); + $draft.trigger(DraftEvents.updatedDraftID); } }; diff --git a/app/assets/javascripts/alaveteli_pro/draft_batch_summary/initialise.js b/app/assets/javascripts/alaveteli_pro/draft_batch_summary/initialise.js index f992c01d2e..3c873787e2 100644 --- a/app/assets/javascripts/alaveteli_pro/draft_batch_summary/initialise.js +++ b/app/assets/javascripts/alaveteli_pro/draft_batch_summary/initialise.js @@ -42,6 +42,31 @@ }); }; + DraftBatchSummary.urlWithDraftID = function(url) { + // Parse the url so that we can modify the draft_id param + var urlParts = url.split('?'); + var path = urlParts[0]; + var querystring = urlParts[1]; + var params = $.deparam(querystring); + + // 1. There is a DraftBatchSummary.draftId, but there is no draft_id param + // in the url, so we want to add the param. + // + // 2. There is a DraftBatchSummary.draftId, and we have an existing + // draft_id param in the url, so we want to update it to make sure we're + // using the current DraftBatchSummary.draftId. + // + // 3. There is no DraftBatchSummary.draftId, but we have a draft_id param + // in the url, so we want to remove the draft_id param. + if (DraftBatchSummary.draftId) { + params.draft_id = DraftBatchSummary.draftId; + } else if (params.draft_id) { + delete params.draft_id; + } + + return path + '?' + $.param(params); + } + var addLoadingClass = function addLoadingClass() { $el.addClass('loading'); }; diff --git a/app/assets/javascripts/alaveteli_pro/existing_batch.js b/app/assets/javascripts/alaveteli_pro/existing_batch.js new file mode 100644 index 0000000000..6e6ed1a46f --- /dev/null +++ b/app/assets/javascripts/alaveteli_pro/existing_batch.js @@ -0,0 +1,9 @@ +$(function() { + $('#ignore_existing_batch').change(function() { + if ($(this).prop('checked')) { + $('#submit_button').removeAttr('disabled', ''); + } else { + $('#submit_button').attr('disabled', 'disabled'); + } + }).change(); +}); diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index 0b48b15a54..d98b8dafc1 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -253,6 +253,14 @@ body.admin { } +/* Timeline */ + +.timeline_date { + .timeline_day { + color: #ddd; + } +} + /* Users */ .user-labels { @@ -260,3 +268,40 @@ body.admin { margin-left: 0.4em; } } + +/* Bootstrap Extensions */ +/* These must come last because we @import bootstrap in .admin */ + +.alert.alert-disabled { + color: #6b6d70; + background-color: #f7f7f9; + border-color: #e1e1e8; + + h4 { + color: #6b6d70; + } +} + +pre.info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +pre.success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +pre.warning { + color: #c09853; + background-color: #fcf8e3; + border-color: #fbeed5; +} + +pre.error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} diff --git a/app/assets/stylesheets/admin/_print_style.scss b/app/assets/stylesheets/admin/_print_style.scss new file mode 100644 index 0000000000..f5f9c75bf8 --- /dev/null +++ b/app/assets/stylesheets/admin/_print_style.scss @@ -0,0 +1,152 @@ +body { + font-size: 10pt; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important; +} + +@page { + margin-top: 2cm; + margin-bottom: 2cm; +} + +#admin-navbar, +.admin-footer, +.btn, +fieldset, +input.search-query, +.help-inline, +.role-filter, +#mass_add_tag_new_tag_substring { + display: none !important; +} + +a { + color: #08c !important; +} + +//Open the accordions when printing +.accordion-body.collapse { + height: auto !important; +} + +.accordion-heading { + padding-bottom: 15px !important; +} + +// Tables styling +table { + page-break-after: auto; + border: 1px solid #d1d1d1; + border-collapse: collapse; + margin-bottom: 20px; +} + +tr { + page-break-inside: avoid; + page-break-after: auto; + -webkit-print-color-adjust: exact; + &:nth-child(odd) { + background-color: #f9f9f9; + } + &:last-child { + border-bottom: none; + } +} + +td { + padding: 4px 5px; + page-break-inside: avoid; + page-break-after: auto; + border-bottom: 1px solid #d1d1d1; +} + +thead { + display: table-header-group; +} + +tfoot { + display: table-footer-group; +} + +// This should make the
elements more subtle +.admin hr { + border-top: 1px solid #eee; + page-break-after: auto; + margin: 20px 0; +} + +// This will avoid fieldset element to have a page-break. +// The fieldset have a display: none on line 21m I'm leaving this code in case +// we decide to make the elemnets visible again. +fieldset { + page-break-inside: avoid; +} + +blockquote { + margin-block-start: 10px; + margin-block-end: 0; + margin-inline-start: 0; + margin-inline-end: 0; + font-weight: 600; +} + +//Users page for admin +#requests, #bodies { + .accordion-heading { + width: 10cm !important; + font-weight: 600; + } + + .item-detail.accordion-body.row { + width: 10cm !important; + border: 1px solid #d1d1d1; + margin-bottom: 25px !important; + + div { + border-bottom: 1px solid #d1d1d1; + padding: 4px 5px; + .span6 { + &:first-child { + margin-right: 5px; + } + } + &:last-child { + border-bottom: none; + } + } + + div:nth-child(odd) { + -webkit-print-color-adjust: exact; + background-color: #eee; + } + } +} + +// Styling for "Stats" page +.hero-unit { + background-color: #f3f3f3; + -webkit-print-color-adjust: exact; + width: 10cm !important; + padding: 10px 15px!important; + border-radius: 3px; + border: 1px solid #e9e9e9; + h2 { + font-weight: 600; + font-size: 9pt; + } +} + +.stats-row { + display: flex; + flex-direction: row; + margin-bottom: 15px !important; + .label-info { + color: #fff; + font-weight: bold; + background-color: #3a87ad; + -webkit-print-color-adjust: exact; + border-radius: 3px !important; + padding: 2px 4px; + margin-top: 10px !important; + margin-right: 5px; + } +} diff --git a/app/assets/stylesheets/admin/print.scss b/app/assets/stylesheets/admin/print.scss new file mode 100644 index 0000000000..e991c58cd0 --- /dev/null +++ b/app/assets/stylesheets/admin/print.scss @@ -0,0 +1 @@ +@import "_print_style"; diff --git a/app/assets/stylesheets/responsive/_global_style.scss b/app/assets/stylesheets/responsive/_global_style.scss index 5fe83bb5fd..119065eead 100644 --- a/app/assets/stylesheets/responsive/_global_style.scss +++ b/app/assets/stylesheets/responsive/_global_style.scss @@ -350,13 +350,15 @@ div.pagination { .action-menu__info-link { a { font-size: 0.625em; //10px + text-transform: uppercase; + font-weight: 700; display: inline-block; line-height: 2.4em; //24px margin-left: 0.8em; background-color: rgba(0,0,0,0.05); border-radius: 3px; padding: 0 0.8em; - color: #2688dc; + color: #1568ae; letter-spacing: 0.1em; position: relative; top: -3px; diff --git a/app/assets/stylesheets/responsive/_lists_style.scss b/app/assets/stylesheets/responsive/_lists_style.scss index 98efc7970e..0e59a74eb6 100644 --- a/app/assets/stylesheets/responsive/_lists_style.scss +++ b/app/assets/stylesheets/responsive/_lists_style.scss @@ -60,7 +60,7 @@ .request_short_listing__authority { font-size: 0.875em; a { - color: #777; + color: #6a6a6a; } } diff --git a/app/assets/stylesheets/responsive/_new_request_layout.scss b/app/assets/stylesheets/responsive/_new_request_layout.scss index 75fe3471bb..5d98bf6320 100644 --- a/app/assets/stylesheets/responsive/_new_request_layout.scss +++ b/app/assets/stylesheets/responsive/_new_request_layout.scss @@ -96,7 +96,16 @@ span#to_public_body { .js-loaded { #request_form_questions { - label { font-size: 1.1em; } + label { + font-size: 1.1em; + padding-left: 20px; + + input[type="radio"] { + // Margin-left: Prevents double line questions to have an uneven vertical aligment. + // Margin-bottom: Fixes the large gap between a question with two lines. + margin: 0 3px 0 -20px; + } + } } .request_form_response { display: none; diff --git a/app/assets/stylesheets/responsive/_password_changes_style.scss b/app/assets/stylesheets/responsive/_password_changes_style.scss new file mode 100644 index 0000000000..e31f343d6a --- /dev/null +++ b/app/assets/stylesheets/responsive/_password_changes_style.scss @@ -0,0 +1,5 @@ +#change_password form{ + input[type="email"] { + width: 280px; + } +} diff --git a/app/assets/stylesheets/responsive/_sidebar_style.scss b/app/assets/stylesheets/responsive/_sidebar_style.scss index f92bee00f5..e550ee40de 100644 --- a/app/assets/stylesheets/responsive/_sidebar_style.scss +++ b/app/assets/stylesheets/responsive/_sidebar_style.scss @@ -57,7 +57,7 @@ display: inline-block; height: 16px; width: 16px; - background-size: 128px 16px; + background-size: 144px 16px; background-repeat: no-repeat; background-image: image-url('act-links-sprite.png'); @media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) { @@ -97,6 +97,10 @@ background-position: -112px 0; } +.act-link-icon--donation { + background-position: -128px 0; +} + .sidebar__section.citations { .citations-list { list-style: none; diff --git a/app/assets/stylesheets/responsive/_wizard_layout.scss b/app/assets/stylesheets/responsive/_wizard_layout.scss index 398ee7f86c..bd0a1c2bbf 100644 --- a/app/assets/stylesheets/responsive/_wizard_layout.scss +++ b/app/assets/stylesheets/responsive/_wizard_layout.scss @@ -80,12 +80,11 @@ */ [data-block="exemption"] fieldset { - max-height: 16em; + height: 16em; @include respond-min( 35em ) { - max-height: 8.3em; + height: 8.3em; } - - transition: max-height 0.2s ease-out; + transition: height 0.2s ease-out; overflow: hidden; } @@ -93,8 +92,11 @@ /* * We're toggling the .expanded class with JS */ - max-height: none; - transition: max-height 0.2s ease-out; + height: auto; + transition: height 0.2s ease-out; + @include respond-min( 35em ) { + height: 16em; + } } .maximise-questions { diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_batch_request_authority_search_layout.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_batch_request_authority_search_layout.scss index 2e6a73a99c..70db895a60 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_batch_request_authority_search_layout.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_batch_request_authority_search_layout.scss @@ -91,6 +91,11 @@ form { margin-bottom: 0; } + + p.batch-builder__actions__count { + margin-right: 10px; + line-height: 0.25em; + } } .batch-builder__list { diff --git a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss index 4a2f2f045b..45e313d9ca 100644 --- a/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss +++ b/app/assets/stylesheets/responsive/alaveteli_pro/_dashboard_style.scss @@ -38,7 +38,7 @@ } .dashboard-activity__item__time { - color: #999; + color: #6a6a6a; } .phase-icon { diff --git a/app/assets/stylesheets/responsive/all.scss b/app/assets/stylesheets/responsive/all.scss index 91fdd1d82b..ccf50f650f 100644 --- a/app/assets/stylesheets/responsive/all.scss +++ b/app/assets/stylesheets/responsive/all.scss @@ -84,6 +84,8 @@ @import "responsive/_time_series"; +@import "responsive/_password_changes_style"; + @import "responsive/alaveteli_pro/_pro_layout"; @import "responsive/alaveteli_pro/_pro_style"; diff --git a/app/controllers/admin/users/sign_ins_controller.rb b/app/controllers/admin/users/sign_ins_controller.rb new file mode 100644 index 0000000000..f03335adc7 --- /dev/null +++ b/app/controllers/admin/users/sign_ins_controller.rb @@ -0,0 +1,15 @@ +# Display information about User::SignIn attempts +class Admin::Users::SignInsController < AdminController + layout 'admin/users' + + def index + @title = 'Listing user sign ins' + + @query = params[:query] + + sign_ins = User::SignIn + sign_ins = sign_ins.search(@query) if @query + + @sign_ins = sign_ins.paginate(page: params[:page], per_page: 100) + end +end diff --git a/app/controllers/admin_comment_controller.rb b/app/controllers/admin_comment_controller.rb index 66b010d845..3598435cf8 100644 --- a/app/controllers/admin_comment_controller.rb +++ b/app/controllers/admin_comment_controller.rb @@ -14,9 +14,9 @@ def index comments = if @query Comment.where(["lower(body) LIKE lower('%'||?||'%')", @query]). - order('created_at DESC') + order(created_at: :desc) else - Comment.order('created_at DESC') + Comment.order(created_at: :desc) end if cannot? :admin, AlaveteliPro::Embargo diff --git a/app/controllers/admin_incoming_message_controller.rb b/app/controllers/admin_incoming_message_controller.rb index 2df8dd6482..1afa80cb98 100644 --- a/app/controllers/admin_incoming_message_controller.rb +++ b/app/controllers/admin_incoming_message_controller.rb @@ -1,6 +1,7 @@ class AdminIncomingMessageController < AdminController before_action :set_incoming_message, :only => [:edit, :update, :destroy, :redeliver] + before_action :set_info_request, :check_info_request def edit end @@ -128,4 +129,15 @@ def set_incoming_message @incoming_message = IncomingMessage.find(params[:id]) end + def set_info_request + @info_request = @incoming_message&.info_request || InfoRequest.find( + params[:request_id] + ) + end + + def check_info_request + return if can? :admin, @info_request + + raise ActiveRecord::RecordNotFound + end end diff --git a/app/controllers/admin_outgoing_message_controller.rb b/app/controllers/admin_outgoing_message_controller.rb index 11f84927ef..ef3c429e0e 100644 --- a/app/controllers/admin_outgoing_message_controller.rb +++ b/app/controllers/admin_outgoing_message_controller.rb @@ -1,6 +1,7 @@ class AdminOutgoingMessageController < AdminController before_action :set_outgoing_message, :only => [:edit, :destroy, :update, :resend] + before_action :set_info_request, :check_info_request before_action :set_is_initial_message, :only => [:edit, :destroy] def edit @@ -91,6 +92,16 @@ def set_outgoing_message @outgoing_message = OutgoingMessage.find(params[:id]) end + def set_info_request + @info_request = @outgoing_message.info_request + end + + def check_info_request + return if can? :admin, @info_request + + raise ActiveRecord::RecordNotFound + end + def set_is_initial_message @is_initial_message = @outgoing_message == last_event_message end diff --git a/app/controllers/admin_public_body_controller.rb b/app/controllers/admin_public_body_controller.rb index 15f016d505..9b5b3a97a7 100644 --- a/app/controllers/admin_public_body_controller.rb +++ b/app/controllers/admin_public_body_controller.rb @@ -18,13 +18,13 @@ def show @locale = AlaveteliLocalization.locale AlaveteliLocalization.with_locale(@locale) do @public_body = PublicBody.find(params[:id]) - info_requests = @public_body.info_requests.order('created_at DESC') + info_requests = @public_body.info_requests.order(created_at: :desc) if cannot? :admin, AlaveteliPro::Embargo info_requests = info_requests.not_embargoed end @info_requests = info_requests.paginate(:page => params[:page], :per_page => 100) - @versions = @public_body.versions.order('version DESC') + @versions = @public_body.versions.order(version: :desc) render end end @@ -124,10 +124,10 @@ def destroy redirect_to admin_bodies_url end - def mass_tag_add + def mass_tag lookup_query - if params[:new_tag] and params[:new_tag] != "" + if params[:tag] and params[:tag] != "" if params[:table_name] == 'exact' bodies = @public_bodies_by_tag elsif params[:table_name] == 'substring' @@ -135,31 +135,19 @@ def mass_tag_add else raise "Unknown table_name #{params[:table_name]}" end - for body in bodies - body.add_tag_if_not_already_present(params[:new_tag]) + + if request.post? + bodies.each { |body| body.add_tag_if_not_already_present(params[:tag]) } + flash[:notice] = 'Added tag to table of bodies.' + elsif request.delete? + bodies.each { |body| body.remove_tag(params[:tag]) } + flash[:notice] = 'Removed tag from table of bodies.' end - flash[:notice] = "Added tag to table of bodies." end redirect_to admin_bodies_url(:query => @query, :page => @page) end - def missing_scheme - # There might be a way to do this in ActiveRecord, but I can't find it - @public_bodies = PublicBody.find_by_sql(" - SELECT a.id, a.name, a.url_name, COUNT(*) AS howmany - FROM public_bodies a JOIN info_requests r ON a.id = r.public_body_id - WHERE a.publication_scheme = '' - GROUP BY a.id, a.name, a.url_name - ORDER BY howmany DESC - LIMIT 20 - ") - @stats = { - "total" => PublicBody.count, - "entered" => PublicBody.where("publication_scheme != ''").count - } - end - def import_csv @notes = "" @errors = "" @@ -276,7 +264,7 @@ def lookup_query PublicBody. joins(:translations). where(query). - order('public_body_translations.name'). + merge(PublicBody::Translation.order(:name)). paginate(:page => @page, :per_page => 100) end diff --git a/app/controllers/admin_raw_email_controller.rb b/app/controllers/admin_raw_email_controller.rb index 7acfa98aef..4b98eaa4e4 100644 --- a/app/controllers/admin_raw_email_controller.rb +++ b/app/controllers/admin_raw_email_controller.rb @@ -8,6 +8,7 @@ class AdminRawEmailController < AdminController skip_before_action :html_response before_action :set_raw_email, only: [:show] + before_action :set_info_request, :check_info_request def show respond_to do |format| @@ -49,6 +50,16 @@ def set_raw_email @raw_email = RawEmail.find(params[:id]) end + def set_info_request + @info_request = @raw_email.incoming_message.info_request + end + + def check_info_request + return if can? :admin, @info_request + + raise ActiveRecord::RecordNotFound + end + def in_holding_pen?(raw_email) raw_email.incoming_message.info_request.holding_pen_request? && !raw_email.empty_from_field? diff --git a/app/controllers/admin_request_controller.rb b/app/controllers/admin_request_controller.rb index f2e82ff9e9..ba76f5e963 100644 --- a/app/controllers/admin_request_controller.rb +++ b/app/controllers/admin_request_controller.rb @@ -6,13 +6,10 @@ class AdminRequestController < AdminController - before_action :set_info_request, :only => [ :show, - :edit, - :update, - :destroy, - :move, - :generate_upload_url, - :hide ] + before_action :set_info_request, :check_info_request, only: %i[ + show edit update destroy move generate_upload_url hide + ] + def index @query = params[:query] if @query @@ -25,15 +22,12 @@ def index info_requests = info_requests.not_embargoed end - @info_requests = info_requests.order('created_at DESC').paginate( + @info_requests = info_requests.order(created_at: :desc).paginate( :page => params[:page], :per_page => 100) end def show - if cannot? :admin, @info_request - raise ActiveRecord::RecordNotFound - end end def edit @@ -87,12 +81,12 @@ def destroy # change user or public body of a request magically def move + editor = admin_current_user + if params[:commit] == 'Move request to user' && !params[:user_url_name].blank? destination_user = User.find_by_url_name(params[:user_url_name]) - if @info_request.move_to_user(destination_user, - :editor => admin_current_user, - :reindex => true) + if @info_request.move_to_user(destination_user, editor: editor) flash[:notice] = "Message has been moved to new user" else flash[:error] = "Couldn't find user '#{params[:user_url_name]}'" @@ -100,11 +94,11 @@ def move redirect_to admin_request_url(@info_request) elsif params[:commit] == 'Move request to authority' && !params[:public_body_url_name].blank? - destination_public_body = PublicBody.find_by_url_name(params[:public_body_url_name]) + destination_body = PublicBody.find_by_url_name( + params[:public_body_url_name] + ) - if @info_request.move_to_public_body(destination_public_body, - :editor => admin_current_user, - :reindex => true) + if @info_request.move_to_public_body(destination_body, editor: editor) flash[:notice] = "Request has been moved to new body" else flash[:error] = "Couldn't find public body '#{ params[:public_body_url_name] }'" @@ -121,7 +115,7 @@ def generate_upload_url if params[:incoming_message_id] incoming_message = IncomingMessage.find(params[:incoming_message_id]) email = incoming_message.from_email - name = incoming_message.safe_mail_from || @info_request.public_body.name + name = incoming_message.safe_from_name || @info_request.public_body.name else email = @info_request.public_body.request_email name = @info_request.public_body.name @@ -147,7 +141,7 @@ def generate_upload_url post_redirect.save! flash[:notice] = { - :partial => "upload_email_message.html.erb", + :partial => "upload_email_message", :locals => { :name => name, :email => email, @@ -214,4 +208,9 @@ def set_info_request @info_request = InfoRequest.find(params[:id].to_i) end + def check_info_request + return if can? :admin, @info_request + + raise ActiveRecord::RecordNotFound + end end diff --git a/app/controllers/admin_track_controller.rb b/app/controllers/admin_track_controller.rb index 4ec9359930..d146388d1e 100644 --- a/app/controllers/admin_track_controller.rb +++ b/app/controllers/admin_track_controller.rb @@ -16,7 +16,7 @@ def index track_things = TrackThing end @admin_tracks = - track_things.order('created_at DESC'). + track_things.order(created_at: :desc). paginate(:page => params[:page], :per_page => 100) @popular = ActiveRecord::Base.connection.select_all("select count(*) as count, title, info_request_id from track_things join info_requests on info_request_id = info_requests.id where info_request_id is not null group by info_request_id, title order by count desc limit 10;") end diff --git a/app/controllers/admin_user_controller.rb b/app/controllers/admin_user_controller.rb index e925410a69..5d6df278ab 100644 --- a/app/controllers/admin_user_controller.rb +++ b/app/controllers/admin_user_controller.rb @@ -5,19 +5,22 @@ # Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class AdminUserController < AdminController + layout 'admin/users' - before_action :set_admin_user, :only => [ :show, - :edit, - :update, - :show_bounce_message, - :clear_bounce, - :clear_profile_photo ] + before_action :set_admin_user, only: %i[show + edit + update + show_bounce_message + clear_bounce + clear_profile_photo] - before_action :clear_roles, + before_action :clear_roles, :clear_features, :check_role_authorisation, - :check_role_requirements, :only => [ :update ] + :check_role_requirements, only: %i[update] def index + @title ||= 'Listing users' + @query = params[:query].try(:strip) @roles = params[:roles] || [] @@ -26,16 +29,8 @@ def index @sort_order = @sort_options.key?(params[:sort_order]) ? params[:sort_order] : 'name_asc' - users = if @query.present? - User.where( - "lower(users.name) LIKE lower('%'||:query||'%') OR " \ - "lower(users.email) LIKE lower('%'||:query||'%') OR " \ - "lower(users.about_me) LIKE lower('%'||:query||'%')", - query: @query - ) - else - User - end + users = @base_scope || User + users = users.search(@query) if @query.present? # with_all_roles returns an array as it takes multiple queries # so we need to requery in order to paginate @@ -47,6 +42,8 @@ def index @admin_users = users.order(@sort_options[@sort_order]). paginate(:page => params[:page], :per_page => 100) + + render action: :index end def show @@ -85,11 +82,22 @@ def update end end + def active + @title = 'Active users' + @base_scope = User.active + index + end + def banned - @banned_users = - User.banned. - order('name ASC'). - paginate(:page => params[:page], :per_page => 100) + @title = 'Banned users' + @base_scope = User.banned + index + end + + def closed + @title = 'Closed users' + @base_scope = User.closed + index end def show_bounce_message @@ -112,8 +120,13 @@ def clear_profile_photo end def modify_comment_visibility - Comment.where(:id => params[:comment_ids]). - update_all(:visible => !params[:hide_selected]) + desired_visibility = params[:hide_selected] ? false : true + + Comment. + where(id: params[:comment_ids]). + where(visible: !desired_visibility). + find_each { |comment| comment.toggle!(:visible) } + redirect_back(fallback_location: admin_users_url) end @@ -123,7 +136,8 @@ def user_params if params[:admin_user] params.require(:admin_user).permit(:name, :email, - {:role_ids => []}, + { role_ids: [] }, + { features: [] }, :ban_text, :about_me, :no_limit, @@ -139,6 +153,11 @@ def clear_roles params[:admin_user][:role_ids] ||= [] end + def clear_features + # Clear features if none checked + params[:admin_user][:features] ||= [] + end + # Check all changed roles exist, current user can grant and revoke them # and requirements are met def check_role_authorisation diff --git a/app/controllers/admin_users_sessions_controller.rb b/app/controllers/admin_users_sessions_controller.rb index 0e5d4cf19f..31b854a285 100644 --- a/app/controllers/admin_users_sessions_controller.rb +++ b/app/controllers/admin_users_sessions_controller.rb @@ -7,20 +7,29 @@ class AdminUsersSessionsController < AdminController def create # Don't use @user as that is any logged in user - @admin_user = User.find(params[:id]) + @user_to_login_as = User.find(params[:id]) - if cannot? :login_as, @admin_user + if cannot? :login_as, @user_to_login_as flash[:error] = - "You don't have permission to log in as #{ @admin_user.name }" - return redirect_to admin_user_path(@admin_user) + "You don't have permission to log in as #{ @user_to_login_as.name }" + return redirect_to admin_user_path(@user_to_login_as) end - @admin_user.confirm! + @user_to_login_as.confirm! - session[:user_id] = @admin_user.id - session[:user_login_token] = @admin_user.login_token + session[:admin_id] = current_user.id + session[:user_id] = @user_to_login_as.id + session[:user_login_token] = @user_to_login_as.login_token session[:user_circumstance] = 'login_as' - redirect_to user_path(@admin_user) + redirect_to user_path(@user_to_login_as) + end + + def destroy + @admin_to_revert_to = User.find(session[:admin_id]) + + sign_in(@admin_to_revert_to) + + redirect_to admin_user_path(current_user) end end diff --git a/app/controllers/alaveteli_pro/info_request_batches_controller.rb b/app/controllers/alaveteli_pro/info_request_batches_controller.rb index 2e2a340f1f..242233336b 100644 --- a/app/controllers/alaveteli_pro/info_request_batches_controller.rb +++ b/app/controllers/alaveteli_pro/info_request_batches_controller.rb @@ -14,7 +14,7 @@ def new def preview @draft_info_request_batch = load_draft load_data_from_draft(@draft_info_request_batch) - if all_models_valid? + if all_models_valid?(ignore_existing_batch: true) render 'alaveteli_pro/info_requests/preview' else remove_duplicate_errors @@ -35,6 +35,8 @@ def create @info_request_batch.save! @draft_info_request_batch.destroy redirect_to show_alaveteli_pro_batch_request_path(id: @info_request_batch.id) + elsif all_models_valid?(ignore_existing_batch: true) + render 'alaveteli_pro/info_requests/preview' else remove_duplicate_errors render 'alaveteli_pro/info_requests/new' @@ -101,7 +103,10 @@ def load_data_from_draft(draft) @outgoing_message = @example_info_request.outgoing_messages.first end - def all_models_valid? + def all_models_valid?(ignore_existing_batch: nil) + ignore_existing_batch ||= params.fetch(:ignore_existing_batch, false) + @info_request_batch.ignore_existing_batch = ignore_existing_batch + @example_info_request.valid? && \ @outgoing_message.valid? && \ (@embargo.nil? || @embargo.present? && @embargo.valid?) && \ diff --git a/app/controllers/alaveteli_pro/info_requests_controller.rb b/app/controllers/alaveteli_pro/info_requests_controller.rb index 3e0bece21f..c3b51d4cfe 100644 --- a/app/controllers/alaveteli_pro/info_requests_controller.rb +++ b/app/controllers/alaveteli_pro/info_requests_controller.rb @@ -129,7 +129,7 @@ def check_public_body_is_requestable @info_request.public_body.is_requestable? reason = @info_request.public_body.not_requestable_reason - view = "request/new_#{reason}.html.erb" + view = "request/new_#{reason}" render view end end diff --git a/app/controllers/alaveteli_pro/subscriptions_controller.rb b/app/controllers/alaveteli_pro/subscriptions_controller.rb index 52317582cb..e6684c9d64 100644 --- a/app/controllers/alaveteli_pro/subscriptions_controller.rb +++ b/app/controllers/alaveteli_pro/subscriptions_controller.rb @@ -120,7 +120,7 @@ def authorise current_user.add_role(:pro) flash[:notice] = { - partial: 'alaveteli_pro/subscriptions/signup_message.html.erb' + partial: 'alaveteli_pro/subscriptions/signup_message' } flash[:new_pro_user] = true diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index bec3b62370..ce69ba9b3a 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -203,7 +203,7 @@ def body_request_events joins(:info_request). where("public_body_id = ?", @public_body.id). includes([{:info_request => :user}, :outgoing_message]). - order('info_request_events.created_at DESC') + order(created_at: :desc) if since_date_str begin diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8d1c892c51..2548c0dca1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -150,10 +150,14 @@ def sign_in(user, remember_me: nil) session[:user_id] = user.id session[:user_login_token] = user.login_token session[:remember_me] = remember_me + # Intentionally allow to fail silently so that we don't have to care whether + # sign in recording is enabled. + user.sign_ins.create(ip: user_ip, country: country_from_ip) end # Logout form def clear_session_credentials + session[:admin_id] = nil session[:user_id] = nil session[:user_login_token] = nil session[:user_circumstance] = nil @@ -225,13 +229,6 @@ def set_in_pro_area private - def user? - warn 'DEPRECATION: ApplicationController#user? will be removed in 0.41. ' \ - 'It has been replaced with authenticated?' - - authenticated? - end - # Override the Rails method to only set the CSRF form token if there is a # logged in user def form_authenticity_token(*args) @@ -239,15 +236,7 @@ def form_authenticity_token(*args) end # Check the user is logged in - def authenticated?(as: nil, **reason_params) - unless reason_params.empty? - warn 'DEPRECATION: ApplicationController#authenticated?(reason_params) ' \ - 'will be removed in 0.41. It has been replaced with ' \ - 'ApplicationController#authenticated? || ' \ - 'ApplicationController#ask_to_login(**reason_params)' - return authenticated?(as: as) || ask_to_login(**reason_params) - end - + def authenticated?(as: nil) if as authenticated_user == as else @@ -288,15 +277,6 @@ def ask_to_login(as: nil, **reason_params) false end - def authenticated_as_user?(user, reason_params = nil) - warn 'DEPRECATION: ApplicationController#authenticated_as_user?(user, ' \ - 'reason_params) will be removed in 0.41. It has been replaced with ' \ - 'ApplicationController#authenticated?(as: user) || ' \ - 'ApplicationController#ask_to_login(as: user, **reason_params)' - - authenticated?(as: user) || ask_to_login(as: user, **reason_params) - end - # Return logged in user def authenticated_user return unless session[:user_id] @@ -385,7 +365,7 @@ def check_read_only if !AlaveteliConfiguration::read_only.empty? if feature_enabled?(:annotations) flash[:notice] = { - :partial => "general/read_only_annotations.html.erb", + :partial => "general/read_only_annotations", :locals => { :site_name => site_name, :read_only => AlaveteliConfiguration.read_only @@ -393,7 +373,7 @@ def check_read_only } else flash[:notice] = { - :partial => "general/read_only.html.erb", + :partial => "general/read_only", :locals => { :site_name => site_name, :read_only => AlaveteliConfiguration.read_only diff --git a/app/controllers/classifications_controller.rb b/app/controllers/classifications_controller.rb index a0c81a6e87..b4e4d48357 100644 --- a/app/controllers/classifications_controller.rb +++ b/app/controllers/classifications_controller.rb @@ -50,7 +50,7 @@ def create # Don't give advice on what to do next, as it isn't their request if session[:request_game] - flash[:notice] = { partial: 'request_game/thank_you.html.erb', + flash[:notice] = { partial: 'request_game/thank_you', locals: { info_request_title: @info_request.title, url: request_path(@info_request) diff --git a/app/controllers/comment_controller.rb b/app/controllers/comment_controller.rb index d913f679b4..533ee8a589 100644 --- a/app/controllers/comment_controller.rb +++ b/app/controllers/comment_controller.rb @@ -5,6 +5,7 @@ # Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class CommentController < ApplicationController + before_action :build_comment, only: [:new] before_action :check_read_only, :only => [ :new ] before_action :find_info_request, :only => [ :new ] before_action :create_track_thing, :only => [ :new ] @@ -13,10 +14,6 @@ class CommentController < ApplicationController before_action :set_in_pro_area, :only => [ :new ] def new - if params[:comment] - @comment = Comment.new(comment_params.merge({ :user => @user })) - end - if params[:comment] # TODO: this check should theoretically be a validation rule in the model @existing_comment = Comment.find_existing(@info_request.id, params[:comment][:body]) @@ -82,6 +79,12 @@ def new private + def build_comment + if params[:comment] + @comment = Comment.new(comment_params.merge(user: @user)) + end + end + def comment_params params.require(:comment).permit(:body) end @@ -116,8 +119,12 @@ def reject_unless_comments_allowed def reject_if_user_banned return unless authenticated? && !authenticated_user.can_make_comments? - @details = authenticated_user.can_fail_html - render template: 'user/banned' + if authenticated_user.exceeded_limit?(:comments) + render template: 'comment/rate_limited' + else + @details = authenticated_user.can_fail_html + render template: 'user/banned' + end end # An override of ApplicationController#set_in_pro_area to set the flag diff --git a/app/controllers/followups_controller.rb b/app/controllers/followups_controller.rb index 24e974cbcd..f558eb7fc0 100644 --- a/app/controllers/followups_controller.rb +++ b/app/controllers/followups_controller.rb @@ -76,7 +76,7 @@ def check_request_matches_incoming_message def check_responses_allowed if @info_request.allow_new_responses_from == "nobody" - flash.now[:error] = { :partial => "followup_not_sent.html.erb", + flash.now[:error] = { :partial => "followup_not_sent", :locals => { :help_contact_path => help_contact_path } } render :action => 'new' diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index df312e8cf2..4ddfa8caff 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -3,14 +3,13 @@ # # Copyright (c) 2008 UK Citizens Online Democracy. All rights reserved. # Email: hello@mysociety.org; WWW: http://www.mysociety.org/ - class HelpController < ApplicationController - - # we don't even have a control subroutine for most help pages, just see their templates + # we don't even have a control subroutine for most help pages, just see their + # templates before_action :long_cache - before_action :catch_spam, :only => [:contact] - before_action :set_recaptcha_required, :only => [:contact] + before_action :catch_spam, only: [:contact] + before_action :set_recaptcha_required, only: [:contact] def index redirect_to help_about_path @@ -20,79 +19,80 @@ def unhappy @country_code = AlaveteliConfiguration.iso_country_code @info_request = nil if params[:url_title] - @info_request = InfoRequest - .not_embargoed - .find_by_url_title!(params[:url_title]) + @info_request = InfoRequest. + not_embargoed. + find_by_url_title!(params[:url_title]) end @refusal_advice = RefusalAdvice.default(@info_request) end def contact - @contact_email = AlaveteliConfiguration::contact_email - if feature_enabled?(:alaveteli_pro) && @user && @user.is_pro? - @contact_email = AlaveteliConfiguration::pro_contact_email - end - # if they clicked remove for link to request/body, remove it if params[:remove] @last_request = nil - cookies["last_request_id"] = nil - cookies["last_body_id"] = nil + cookies['last_request_id'] = nil + cookies['last_body_id'] = nil end # look up link to request/body - request = InfoRequest.find_by(id: cookies["last_request_id"].to_i) + request = InfoRequest.find_by(id: cookies['last_request_id'].to_i) @last_request = request if can?(:read, request) - @last_body = PublicBody.find_by(id: cookies["last_body_id"].to_i) + @last_body = PublicBody.find_by(id: cookies['last_body_id'].to_i) # submit form - if params[:submitted_contact_form] - if @user - params[:contact][:email] = @user.email - params[:contact][:name] = @user.name - end - @contact = ContactValidator.new(params[:contact]) - - if (@recaptcha_required && - !params[:remove] && - !verify_recaptcha) - flash.now[:error] = _('There was an error with the reCAPTCHA. ' \ - 'Please try again.') - elsif @contact.valid? && !params[:remove] - ContactMailer.to_admin_message( - params[:contact][:name], - params[:contact][:email], - params[:contact][:subject], - params[:contact][:message], - @user, - @last_request, @last_body - ).deliver_now - flash[:notice] = _("Your message has been sent. Thank you for getting in touch! We'll get back to you soon.") - redirect_to frontpage_url - return - end - - if params[:remove] - @contact.errors.clear - end + return unless params[:submitted_contact_form] + + if @user + params[:contact][:email] = @user.email + params[:contact][:name] = @user.name end + if params[:remove] + contact_validator.errors.clear + + elsif @recaptcha_required && !verify_recaptcha + flash.now[:error] = _('There was an error with the reCAPTCHA. ' \ + 'Please try again.') + elsif contact_validator.valid? + contact_mailer.deliver_now + flash[:notice] = _("Your message has been sent. Thank you for getting " \ + "in touch! We'll get back to you soon.") + redirect_to frontpage_url + end end private + def contact_validator + @contact_validator ||= ContactValidator.new(contact_params) + end + + def contact_mailer + ContactMailer.to_admin_message( + contact_params[:name], + contact_params[:email], + contact_params[:subject], + contact_params[:message], + @user, @last_request, @last_body + ) + end + + def contact_params + params.require(:contact).except(:comment).permit( + :name, :email, :subject, :message + ) + end + def catch_spam - if request.post? && params[:contact] - if !params[:contact][:comment].blank? || !params[:contact].key?(:comment) - redirect_to frontpage_url - end - end + return unless request.post? && params[:contact] + return if params[:contact].fetch(:comment, '').blank? + + redirect_to frontpage_url end def set_recaptcha_required @recaptcha_required = AlaveteliConfiguration.contact_form_recaptcha end - end diff --git a/app/controllers/request_controller.rb b/app/controllers/request_controller.rb index 51832cfeed..4486c38f63 100644 --- a/app/controllers/request_controller.rb +++ b/app/controllers/request_controller.rb @@ -222,8 +222,8 @@ def new_batch @public_bodies = PublicBody. where(:id => params[:public_body_ids]). - includes(:translations). - order('public_body_translations.name') + joins(:translations).preload(:translations). + merge(PublicBody::Translation.order(:name)) end if params[:submitted_new_request].nil? || params[:reedit] @@ -292,7 +292,7 @@ def new # logged in and we want to include the text of the request so they # can squirrel it away for tomorrow, so we detect this later after # we have constructed the InfoRequest. - user_exceeded_limit = authenticated_user.exceeded_limit? + user_exceeded_limit = authenticated_user.exceeded_limit?(:info_requests) if !user_exceeded_limit @details = authenticated_user.can_fail_html render :template => 'user/banned' @@ -677,28 +677,28 @@ def make_request_zip(info_request, file_path) def make_request_summary_file(info_request) done = false - convert_command = AlaveteliConfiguration::html_to_pdf_command @render_to_file = true assign_variables_for_show_template(info_request) - if !convert_command.blank? && File.exist?(convert_command) + if HTMLtoPDFConverter.exist? html_output = render_to_string(:template => 'request/show') tmp_input = Tempfile.new(['foihtml2pdf-input', '.html']) tmp_input.write(html_output) tmp_input.close tmp_output = Tempfile.new('foihtml2pdf-output') - output = AlaveteliExternalCommand.run(convert_command, tmp_input.path, tmp_output.path) + command = HTMLtoPDFConverter.new(tmp_input, tmp_output) + output = command.run if !output.nil? file_info = { :filename => 'correspondence.pdf', :data => File.open(tmp_output.path).read } done = true else - logger.error("Could not convert info request #{info_request.id} to PDF with command '#{convert_command} #{tmp_input.path} #{tmp_output.path}'") + logger.error("Could not convert info request #{info_request.id} to PDF with command '#{command}'") end tmp_output.close tmp_input.delete tmp_output.delete else - logger.warn("No HTML -> PDF converter found at #{convert_command}") + logger.warn("No HTML -> PDF converter found") end if !done file_info = { :filename => 'correspondence.txt', @@ -816,7 +816,7 @@ def render_new_compose(batch) def render_new_preview if @outgoing_message.contains_email? || @outgoing_message.contains_postcode? flash.now[:error] = { - :partial => "preview_errors.html.erb", + :partial => "preview_errors", :locals => { :contains_email => @outgoing_message.contains_email?, :contains_postcode => @outgoing_message.contains_postcode?, @@ -933,8 +933,9 @@ def block_restricted_country_ips? def handle_blocked_ip(info_request) if send_exception_notifications? - e = Exception.new("Possible spam (ip_in_blocklist) from #{ info_request.user_id }: #{ info_request.title }") - ExceptionNotifier.notify_exception(e, :env => request.env) + msg = "Possible spam request (ip_in_blocklist) from " \ + "User##{info_request.user_id}: #{user_ip} (#{country_from_ip})" + ExceptionNotifier.notify_exception(Exception.new(msg), env: request.env) end if block_restricted_country_ips? diff --git a/app/controllers/request_game_controller.rb b/app/controllers/request_game_controller.rb index e309018c7c..3e4530d297 100644 --- a/app/controllers/request_game_controller.rb +++ b/app/controllers/request_game_controller.rb @@ -29,7 +29,7 @@ def play if @missing == 0 flash.now[:notice] = { - :partial => "request_game/game_over.html.erb", + :partial => "request_game/game_over", :locals => { :helpus_url => help_credits_path(:anchor => "helpus"), :site_name => site_name diff --git a/app/controllers/track_controller.rb b/app/controllers/track_controller.rb index 9ea449ee9a..65fdbf5165 100644 --- a/app/controllers/track_controller.rb +++ b/app/controllers/track_controller.rb @@ -150,11 +150,7 @@ def track_set return true else # this will most likely be tripped by a single error - probably track_query length - if rails_upgrade? - flash[:error] = @track_thing.errors.map { |e| e.message }.join(", ") - else - flash[:error] = @track_thing.errors.map { |_, msg| msg }.join(", ") - end + flash[:error] = @track_thing.errors.map(&:message).join(", ") return false end end diff --git a/app/controllers/user_controller.rb b/app/controllers/user_controller.rb index 22d7f2d1af..a78de197f7 100644 --- a/app/controllers/user_controller.rb +++ b/app/controllers/user_controller.rb @@ -34,6 +34,7 @@ def show set_view_instance_variables @same_name_users = User.find_similar_named_users(@display_user) @is_you = current_user_is_display_user + @show_about_me = show_about_me? set_show_requests if @show_requests @@ -62,7 +63,7 @@ def show # All tracks for the user @track_things = TrackThing. where(:tracking_user_id => @display_user, :track_medium => 'email_daily'). - order('created_at desc') + order(created_at: :desc) @track_things_grouped = @track_things.group_by(&:track_type) # Requests you need to describe @undescribed_requests = @display_user.get_undescribed_requests @@ -101,7 +102,7 @@ def wall @track_things = TrackThing. where(:tracking_user_id => @display_user.id, :track_medium => 'email_daily'). - order('created_at desc') + order(created_at: :desc) @track_things.each do |track_thing| # TODO: factor out of track_mailer.rb xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], track_thing.track_query, @@ -139,11 +140,7 @@ def signup if user_alreadyexists # attempt to remove the 'already in use message' from the errors hash # so it doesn't get accidentally shown to the end user - if rails_upgrade? - @user_signup.errors.delete(:email, :taken) - else - @user_signup.errors[:email].delete_if { |message| message == _("This email is already in use") } - end + @user_signup.errors.delete(:email, :taken) end if error || !@user_signup.errors.empty? # Show the form @@ -155,6 +152,13 @@ def signup else # New unconfirmed user + # Block signups from suspicious countries + # TODO: Add specs (see RequestController#create) + # TODO: Extract to UserSpamScorer? + if blocked_ip?(country_from_ip, @user_signup) + handle_blocked_ip(@user_signup) && return + end + # Rate limit signups ip_rate_limiter.record(user_ip) @@ -180,6 +184,17 @@ def signup render action: :sign end + # A webserver level redirect can be used to redirect from the signup action to + # prevent spam signups from Tor. + def tor + long_cache + + msg = _('Signups from Tor have been blocked due to extensive misuse. ' \ + 'Please contact us if this is a problem for you.') + + render plain: msg, status: :forbidden + end + def ip_rate_limiter @ip_rate_limiter ||= AlaveteliRateLimiter::IPRateLimiter.new(:signup) end @@ -325,7 +340,7 @@ def set_profile_photo if @user.get_about_me_for_html_display.empty? - flash[:notice] = { :partial => "user/update_profile_photo.html.erb" } + flash[:notice] = { :partial => "user/update_profile_photo" } redirect_to edit_profile_about_me_url else flash[:notice] = _("Thank you for updating your profile photo") @@ -386,6 +401,31 @@ def set_receive_email_alerts private + def block_restricted_country_ips? + AlaveteliConfiguration.block_restricted_country_ips || + AlaveteliConfiguration.enable_anti_spam + end + + def blocked_ip?(country, user) + AlaveteliConfiguration.restricted_countries.include?(country) && + country != AlaveteliConfiguration.iso_country_code + end + + def handle_blocked_ip(user) + if send_exception_notifications? + msg = "Possible spam signup (ip_in_blocklist) from " \ + "#{user.email}: #{user_ip} (#{country_from_ip})" + ExceptionNotifier.notify_exception(Exception.new(msg), env: request.env) + end + + if block_restricted_country_ips? + flash.now[:error] = _("Sorry, we're currently unable to create your " \ + "account. Please try again later.") + render action: 'sign' + true + end + end + def set_request_from_foreign_country @request_from_foreign_country = country_from_ip != AlaveteliConfiguration.iso_country_code @@ -537,6 +577,15 @@ def current_user_is_display_user @user.try(:id) == @display_user.id end + def show_about_me? + return true if @is_you + return false unless @display_user.get_about_me_for_html_display.present? + return false unless @display_user.active? + return true if @display_user.confirmed_not_spam? + return true if @user + false + end + # Redirects to front page later if nothing else specified def generate_post_redirect_for_signup(redirect_to="/") redirect_to = "/" if redirect_to.nil? diff --git a/app/controllers/user_profile/about_me_controller.rb b/app/controllers/user_profile/about_me_controller.rb index d132e656aa..981faaaeaa 100644 --- a/app/controllers/user_profile/about_me_controller.rb +++ b/app/controllers/user_profile/about_me_controller.rb @@ -32,7 +32,7 @@ def update flash[:notice] = _("You have now changed the text about you on your profile.") redirect_to user_url(@user) else - flash[:notice] = { :partial => "update_profile_text.html.erb" } + flash[:notice] = { :partial => "update_profile_text" } redirect_to set_profile_photo_url end else diff --git a/app/helpers/admin/bootstrap_helper.rb b/app/helpers/admin/bootstrap_helper.rb new file mode 100644 index 0000000000..e5367a8795 --- /dev/null +++ b/app/helpers/admin/bootstrap_helper.rb @@ -0,0 +1,14 @@ +# Helpers for working with Bootstrap elements within the admin interface +module Admin::BootstrapHelper + def nav_li(path) + tag.li class: nav_li_class(path) do + yield + end + end + + private + + def nav_li_class(path) + 'active' if current_page?(path) + end +end diff --git a/app/helpers/admin/censor_rules_helper.rb b/app/helpers/admin/censor_rules_helper.rb new file mode 100644 index 0000000000..0c62648fb7 --- /dev/null +++ b/app/helpers/admin/censor_rules_helper.rb @@ -0,0 +1,12 @@ +# Helpers for dealing with CensorRules in the admin interface +module Admin::CensorRulesHelper + def censor_rule_applies_to(censor_rule) + censorable = censor_rule.censorable + censorable ? both_links(censorable) : tag.strong('everything') + end + + def censor_rule_applicable_class(censor_rule) + censorable = censor_rule.censorable + censorable ? censorable.class.to_s : 'Global' + end +end diff --git a/app/helpers/admin/link_helper.rb b/app/helpers/admin/link_helper.rb new file mode 100644 index 0000000000..6abda26e8b --- /dev/null +++ b/app/helpers/admin/link_helper.rb @@ -0,0 +1,55 @@ +# Helpers for rendering record links in the admin interface +module Admin::LinkHelper + def both_links(record) + method = "#{record.class.to_s.underscore}_both_links" + send(method, record) + end + + private + + def info_request_both_links(info_request) + title = 'View request on public website' + icon = prominence_icon(info_request) + + link_to(icon, request_path(info_request), title: title) + ' ' + + link_to(info_request.title, admin_request_path(info_request), + title: admin_title) + end + + def info_request_batch_both_links(batch) + title = 'View batch on public website' + icon = prominence_icon(batch) + + link_to(icon, batch, title: title) + ' ' + batch.title + end + + def public_body_both_links(public_body) + title = 'View authority on public website' + icon = eye + + link_to(icon, public_body_path(public_body), title: title) + ' ' + + link_to(public_body.name, admin_body_path(public_body), + title: admin_title) + end + + def user_both_links(user) + title = 'View user on public website' + icon = prominence_icon(user) + + link_to(icon, user_path(user), title: title) + ' ' + + link_to(user.name, admin_user_path(user), title: admin_title) + end + + def comment_both_links(comment) + title = 'View comment on public website' + icon = prominence_icon(comment) + + link_to(icon, comment_path(comment), title: title) + ' ' + + link_to(truncate(comment.body), edit_admin_comment_path(comment), + title: admin_title) + end + + def admin_title + 'View full details' + end +end diff --git a/app/helpers/admin/translated_record_form.rb b/app/helpers/admin/translated_record_form.rb index 6c45e1be13..db0ed2da7e 100644 --- a/app/helpers/admin/translated_record_form.rb +++ b/app/helpers/admin/translated_record_form.rb @@ -73,13 +73,8 @@ def locale_fields(t, locale) end def default_locale_error_messages - if rails_upgrade? - default_locale_errors = object.errors.reject do |error| - error.attribute.starts_with?('translation') - end - else - default_locale_errors = - object.errors.reject { |attr, _| attr.to_s.starts_with?('translation') } + default_locale_errors = object.errors.reject do |error| + error.attribute.starts_with?('translation') end @template.concat(errors_for(default_locale, default_locale_errors)) @@ -98,22 +93,12 @@ def errors_for(locale, errors) @template.concat(locale_name(locale)) ul = @template.tag.ul do - if rails_upgrade? - errors.each do |error| - content = @template.tag.li do - "#{ error.attribute } #{ error.message }".html_safe - end - - @template.concat(content) + errors.each do |error| + content = @template.tag.li do + "#{ error.attribute } #{ error.message }".html_safe end - else - errors.each do |attr, message| - content = @template.tag.li do - "#{ attr } #{ message }".html_safe - end - @template.concat(content) - end + @template.concat(content) end end diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 921d7967a1..2b0fed840a 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -1,4 +1,9 @@ module AdminHelper + include Admin::BootstrapHelper + include Admin::CensorRulesHelper + include Admin::LinkHelper + include Admin::ProminenceHelper + def icon(name) content_tag(:i, "", :class => "icon-#{name}") end @@ -19,28 +24,6 @@ def arrow_right icon("arrow-right") end - def request_both_links(info_request) - link_to(eye, request_path(info_request), :title => "view request on public website") + " " + - link_to(info_request.title, admin_request_path(info_request), :title => "view full details") - end - - def public_body_both_links(public_body) - link_to(eye, public_body_path(public_body), :title => "view authority on public website") + " " + - link_to(h(public_body.name), admin_body_path(public_body), :title => "view full details") - end - - def user_both_links(user) - link_to(eye, user_path(user), :title => "view user's page on public website") + " " + - link_to(h(user.name), admin_user_path(user), :title => "view full details") - end - - def comment_both_links(comment) - link_to(eye, comment_path(comment), - :title => "view comment on public website") + " " + - link_to(h(truncate(comment.body)), edit_admin_comment_path(comment), - :title => "view full details") - end - def comment_visibility(comment) comment.visible? ? 'Visible' : 'Hidden' end diff --git a/app/helpers/alaveteli_pro/batch_request_authority_searches_helper.rb b/app/helpers/alaveteli_pro/batch_request_authority_searches_helper.rb index c09fe7f1b6..8b36f749d1 100644 --- a/app/helpers/alaveteli_pro/batch_request_authority_searches_helper.rb +++ b/app/helpers/alaveteli_pro/batch_request_authority_searches_helper.rb @@ -5,5 +5,34 @@ def batch_notes_allowed_tags Alaveteli::Application.config.action_view.sanitized_allowed_tags - %w(pre h1 h2 h3 h4 h5 h6 img blockquote html head body style) end + + def batch_authority_count + count = @draft_batch_request.public_bodies.count + + tag_attributes = { + class: %w[batch-builder__actions__count], + data: { + message_template_zero: authority_count(count_override: 0), + message_template_one: authority_count(count_override: 1), + message_template_many: authority_count(count_override: 2) + } + } + + content_tag :p, authority_count, tag_attributes + end + + private + + def authority_count(count_override: nil) + limit = AlaveteliConfiguration.pro_batch_authority_limit + + count = count_override || @draft_batch_request.public_bodies.count + count_text = '{{count}}' if count_override + count_text ||= count + + n_("{{count}} of {{limit}} authorities", + "{{count}} of {{limit}} authorities", + count, count: count_text, limit: limit) + end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 87b78f91b9..04f6019936 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -51,14 +51,8 @@ def foi_error_messages_for(*params) error_messages = "".html_safe objects.each do |object| - if rails_upgrade? - object.errors.each do |error| - error_messages << content_tag(:li, h(error.message)) - end - else - object.errors.each do |attr, message| - error_messages << content_tag(:li, h(message)) - end + object.errors.each do |error| + error_messages << content_tag(:li, h(error.message)) end end @@ -140,9 +134,13 @@ def cache_if_caching_fragments(*args, &block) end end - def render_flash(flash) - flash = { :plain => flash } if flash.is_a?(String) - render flash.deep_symbolize_keys + def inside_layout(layout = 'application', &block) + render inline: capture(&block), layout: "layouts/#{layout}" + end + + def render_flash(message) + message = { plain: message } if message.is_a?(String) + render message.deep_symbolize_keys end # We only want to cache request lists that have a reasonable chance of not expiring diff --git a/app/mailers/outgoing_mailer.rb b/app/mailers/outgoing_mailer.rb index 1f27813d9f..bf11b55dbf 100644 --- a/app/mailers/outgoing_mailer.rb +++ b/app/mailers/outgoing_mailer.rb @@ -47,8 +47,8 @@ def self.name_and_email_for_followup(info_request, incoming_message_followup) if incoming_message_followup.nil? || !incoming_message_followup.valid_to_reply_to? return info_request.recipient_name_and_email else - # calling safe_mail_from from so censor rules are run - return MailHandler.address_from_name_and_email(incoming_message_followup.safe_mail_from, + # calling safe_from_name from so censor rules are run + return MailHandler.address_from_name_and_email(incoming_message_followup.safe_from_name, incoming_message_followup.from_email) end end @@ -57,8 +57,8 @@ def self.name_for_followup(info_request, incoming_message_followup) if incoming_message_followup.nil? || !incoming_message_followup.valid_to_reply_to? return info_request.public_body.name else - # calling safe_mail_from from so censor rules are run - return incoming_message_followup.safe_mail_from || info_request.public_body.name + # calling safe_from_name from so censor rules are run + return incoming_message_followup.safe_from_name || info_request.public_body.name end end # Used when making list of followup places to remove duplicates diff --git a/app/mailers/request_mailer.rb b/app/mailers/request_mailer.rb index 73d52d3540..3e88f50bb7 100644 --- a/app/mailers/request_mailer.rb +++ b/app/mailers/request_mailer.rb @@ -354,7 +354,7 @@ def self.alert_new_response_reminders_internal(days_since, type_code) info_requests = InfoRequest. where_old_unclassified(days_since). where(use_notifications: false). - order('info_requests.id'). + order(:id). includes(:user) info_requests.each do |info_request| @@ -405,7 +405,7 @@ def self.alert_not_clarified_request Time.zone.now - 3.days, false ). - includes(:user).order("info_requests.id") + includes(:user).order(:id) for info_request in info_requests alert_event_id = info_request.get_last_public_response_event_id last_response_message = info_request.get_last_public_response @@ -468,7 +468,7 @@ def self.alert_comment_on_request InfoRequest. includes(:info_request_events => :user_info_request_sent_alerts). where(conditions). - order('info_requests.id, info_request_events.created_at'). + order(:id).merge(InfoRequestEvent.order(:created_at)). references(:info_request_events) info_requests.each do |info_request| diff --git a/app/models/alaveteli_pro/request_filter.rb b/app/models/alaveteli_pro/request_filter.rb index e82ce8d4eb..7f9f4b3f33 100644 --- a/app/models/alaveteli_pro/request_filter.rb +++ b/app/models/alaveteli_pro/request_filter.rb @@ -43,7 +43,8 @@ def results(user) q: "%#{ search }%") .references(:request_summary_categories) request_summaries = filter_results(request_summaries) - request_summaries.reorder("request_summaries.#{order_value}") + request_summaries. + merge(RequestSummary.order(order_value)) end def filter_results(results) diff --git a/app/models/censor_rule.rb b/app/models/censor_rule.rb index c653c88b54..bd93a4cd77 100644 --- a/app/models/censor_rule.rb +++ b/app/models/censor_rule.rb @@ -23,7 +23,16 @@ # Email: hello@mysociety.org; WWW: http://www.mysociety.org/ class CensorRule < ApplicationRecord + DEFAULT_CANNED_REPLACEMENTS = [ + _('[Personally Identifiable Information removed]'), + _('[name removed]'), + _('[extraneous material removed]'), + _('[potentially defamatory material removed]'), + _('[extraneous and potentially defamatory material removed]') + ].freeze + include AdminColumn + belongs_to :info_request, :inverse_of => :censor_rules belongs_to :user, @@ -44,6 +53,10 @@ class CensorRule < ApplicationRecord public_body_id: nil) } + cattr_accessor :canned_replacements, + instance_writer: false, + default: DEFAULT_CANNED_REPLACEMENTS.dup + def apply_to_text(text_to_censor) return nil if text_to_censor.nil? text_to_censor.gsub(to_replace('UTF-8'), replacement) @@ -89,6 +102,10 @@ def censorable_requests end end + def censorable + info_request || user || public_body || nil + end + private def single_char_regexp diff --git a/app/models/comment.rb b/app/models/comment.rb index 948b262841..8583da961e 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,5 +1,5 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20220210114052 # # Table name: comments # @@ -7,11 +7,11 @@ # user_id :integer not null # info_request_id :integer # body :text not null -# visible :boolean default("true"), not null +# visible :boolean default(TRUE), not null # created_at :datetime not null # updated_at :datetime not null # locale :text default(""), not null -# attention_requested :boolean default("false"), not null +# attention_requested :boolean default(FALSE), not null # # models/comments.rb: @@ -63,7 +63,7 @@ class Comment < ApplicationRecord references(:embargoes) } - after_save :event_xapian_update + after_save :reindex_request_events default_url_options[:host] = AlaveteliConfiguration.domain @@ -97,15 +97,24 @@ def body ret end + def prominence + hidden? ? 'hidden' : 'normal' + end + def hidden? !visible? end - # So when takes changes it updates, or when made invisble it vanishes - def event_xapian_update + def reindex_request_events info_request_events.find_each(&:xapian_mark_needs_index) end + def event_xapian_update + warn 'DEPRECATION: Comment#event_xapian_update will be removed in 0.42. ' \ + 'It has been replaced with Comment#reindex_request_events' + reindex_request_events + end + # Return body for display as HTML def get_body_for_html_display text = body.strip @@ -187,6 +196,18 @@ def last_reported_at last_report.try(:created_at) end + def hide(editor:) + ActiveRecord::Base.transaction do + event_params = { comment_id: id, + editor: editor.url_name, + old_visible: visible?, + visible: false } + + update!(visible: false) + info_request.log_event('hide_comment', event_params) + end + end + private def check_body_has_content diff --git a/app/models/concerns/taggable.rb b/app/models/concerns/taggable.rb index 4a7445f5ad..0139429493 100644 --- a/app/models/concerns/taggable.rb +++ b/app/models/concerns/taggable.rb @@ -21,7 +21,7 @@ def self.tag_search_sql(tag) select(1). where("has_tag_string_tags.model_id = #{quoted_table_name}." \ "#{quoted_primary_key}"). - where("has_tag_string_tags.model = '#{self}'"). + where("has_tag_string_tags.model_type = '#{self}'"). where(name: name) scope = scope.where(value: value) if value scope.to_sql @@ -29,7 +29,7 @@ def self.tag_search_sql(tag) private_class_method :tag_search_sql def self.tags - HasTagString::HasTagStringTag.where(model_id: all, model: to_s). + HasTagString::HasTagStringTag.where(model_id: all, model_type: to_s). map(&:name_and_value) end end diff --git a/app/models/foi_attachment.rb b/app/models/foi_attachment.rb index 3f62af9509..2d5f5dfb33 100644 --- a/app/models/foi_attachment.rb +++ b/app/models/foi_attachment.rb @@ -29,6 +29,8 @@ class FoiAttachment < ApplicationRecord belongs_to :incoming_message, :inverse_of => :foi_attachments + has_one_attached :file, service: :attachments + validates_presence_of :content_type validates_presence_of :filename validates_presence_of :display_size @@ -42,32 +44,54 @@ class FoiAttachment < ApplicationRecord BODY_MAX_DELAY = 5 def directory + if file.attached? + warn <<~DEPRECATION.squish + [DEPRECATION] FoiAttachment#directory shouldn't be used when using + `ActiveStorage` backed file stores. This method will be removed + in 0.42. + DEPRECATION + return + end + base_dir = File.expand_path(File.join(File.dirname(__FILE__), "../../cache", "attachments_#{Rails.env}")) return File.join(base_dir, self.hexdigest[0..2]) end def filepath + if file.attached? + warn <<~DEPRECATION.squish + [DEPRECATION] FoiAttachment#filepath shouldn't be used when using + `ActiveStorage` backed file stores. This method will be removed + in 0.42. + DEPRECATION + return + end + File.join(self.directory, self.hexdigest) end def delete_cached_file! - begin - @cached_body = nil - File.delete(self.filepath) - rescue + @cached_body = nil + + if file.attached? + file.purge + elsif File.exist?(filepath) + File.delete(filepath) end end def body=(d) self.hexdigest = Digest::MD5.hexdigest(d) - if !File.exist?(self.directory) - FileUtils.mkdir_p self.directory - end - File.open(self.filepath, "wb") { |file| - file.write d - } - update_display_size! + + ensure_filename! + file.attach( + io: StringIO.new(d.to_s), + filename: filename, + content_type: content_type + ) + @cached_body = d.force_encoding("ASCII-8BIT") + update_display_size! end # raw body, encoded as binary @@ -76,8 +100,12 @@ def body tries = 0 delay = 1 begin - @cached_body = File.open(filepath, "rb" ) { |file| file.read } - rescue Errno::ENOENT + if file.attached? + @cached_body = file.download + else + @cached_body = File.open(filepath, "rb" ) { |file| file.read } + end + rescue Errno::ENOENT, ActiveStorage::FileNotFoundError # we've lost our cached attachments for some reason. Reparse them. if tries > BODY_MAX_TRIES raise @@ -89,6 +117,7 @@ def body delay = BODY_MAX_DELAY if delay > BODY_MAX_DELAY force = true self.incoming_message.parse_raw_email!(force) + reload retry end end diff --git a/app/models/incoming_message.rb b/app/models/incoming_message.rb index a2d79c606e..f9eefe7920 100644 --- a/app/models/incoming_message.rb +++ b/app/models/incoming_message.rb @@ -1,5 +1,5 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20220210120801 # # Table name: incoming_messages # @@ -12,13 +12,14 @@ # cached_main_body_text_folded :text # cached_main_body_text_unfolded :text # subject :text -# mail_from_domain :text +# from_email_domain :text # valid_to_reply_to :boolean # last_parsed :datetime -# mail_from :text +# from_name :text # sent_at :datetime # prominence :string default("normal"), not null # prominence_reason :text +# from_email :text # # models/incoming_message.rb: @@ -38,6 +39,7 @@ class IncomingMessage < ApplicationRecord include AdminColumn include MessageProminence + include CacheAttributesFromRawEmail MAX_ATTACHMENT_TEXT_CLIPPED = 1000000 # 1Mb ish @@ -55,9 +57,10 @@ class IncomingMessage < ApplicationRecord :class_name => 'OutgoingMessage', :dependent => :nullify has_many :foi_attachments, - -> { order('id') }, - :inverse_of => :incoming_message, - :dependent => :destroy + -> { order(:id) }, + inverse_of: :incoming_message, + dependent: :destroy, + autosave: true # never really has many info_request_events, but could in theory has_many :info_request_events, :dependent => :destroy, @@ -74,16 +77,19 @@ class IncomingMessage < ApplicationRecord scope :pro, -> { joins(:info_request).merge(InfoRequest.pro) } scope :unparsed, -> { where(last_parsed: nil) } - delegate :from_email, to: :raw_email + cache_from_raw_email :subject, :sent_at, + :from_name, :from_email, :from_email_domain, + :valid_to_reply_to + delegate :message_id, to: :raw_email delegate :multipart?, to: :raw_email delegate :parts, to: :raw_email delegate :legislation, to: :info_request - # Given that there are in theory many info request events, a convenience method for - # getting the response event + # Given that there are in theory many info request events, a convenience + # method for getting the response event. def response_event - self.info_request_events.detect { |e| e.event_type == 'response' } + info_request_events.where(event_type: 'response').first end def parse_raw_email!(force = nil) @@ -95,19 +101,14 @@ def parse_raw_email!(force = nil) end if (!force.nil? || self.last_parsed.nil?) ActiveRecord::Base.transaction do - self.extract_attachments! + extract_attachments self.sent_at = raw_email.date || created_at self.subject = raw_email.subject - self.mail_from = raw_email.from_name - if from_email - self.mail_from_domain = - PublicBody.extract_domain_from_email(from_email) - else - self.mail_from_domain = "" - end + self.from_name = raw_email.from_name + self.from_email = raw_email.from_email || '' + self.from_email_domain = raw_email.from_email_domain || '' self.valid_to_reply_to = raw_email.valid_to_reply_to? self.last_parsed = Time.zone.now - self.foi_attachments.reload self.save! end end @@ -117,66 +118,12 @@ def destroy_email_file raw_email.destroy_file_representation! end - # The cached fields mentioned in the previous comment - - # Public: Can this message be replied to? - # Caches the value set by raw_email.valid_to_reply_to? in #parse_raw_email! - # #valid_to_reply_to overrides the ActiveRecord provided #valid_to_reply_to - # - # Returns a Boolean - def valid_to_reply_to - parse_raw_email! - super - end - alias_method :valid_to_reply_to?, :valid_to_reply_to - # Public: The date and time the email was sent. Uses the Date header if - # present in the email, otherwise uses the record's created_at attribute. - # #sent_at overrides the ActiveRecord provided #sent_at - # - # Returns an ActiveSupport::TimeWithZone - def sent_at - parse_raw_email! - super - end - - # Public: The subject of an email. - # #subject overrides the ActiveRecord provided #subject - # - # Examples: - # - # # Subject: A response to your FOI request - # incoming_message.subject - # # => 'A response to your FOI request' - # - # # No subject header - # incoming_message.subject - # # => nil - # - # Returns a String or nil - def subject - parse_raw_email! - super - end - - # Public: The display name of the email sender. - # #mail_from overrides the ActiveRecord provided #mail_from - # - # Examples: - # - # # From: John Doe - # incoming_message.mail_from - # # => 'John Doe' - # - # # From: john@example.com - # incoming_message.mail_from - # # => nil - # - # Returns a String or nil def mail_from - parse_raw_email! - super + warn %q([DEPRECATION] IncomingMessage#mail_from will be removed in 0.42. It + has been replaced by IncomingMessage#from_name).squish + from_name end # Public: The display name of the email sender with the associated @@ -186,42 +133,36 @@ def mail_from # # # Given a CensorRule that redacts the word 'Person': # - # incoming_message.mail_from + # incoming_message.from_name # # => FOI Person # - # incoming_message.safe_mail_from + # incoming_message.safe_from_name # # => FOI [REDACTED] # # Returns a String def safe_mail_from - if mail_from - info_request.apply_censor_rules_to_text(mail_from) - end + warn %q([DEPRECATION] IncomingMessage#safe_mail_from will be removed in + 0.42. It has been replaced by IncomingMessage#safe_from_name).squish + safe_from_name + end + + def safe_from_name + info_request.apply_censor_rules_to_text(from_name) if from_name end - # Public: The domain part of the email address in the From header. - # #mail_from_domain overrides the ActiveRecord provided #mail_from_domain - # - # # From: John Doe - # incoming_message.mail_from_domain - # # => 'example.com' - # - # # No From header - # incoming_message.mail_from_domain - # # => '' - # - # Returns a String def mail_from_domain - parse_raw_email! - super + warn %q([DEPRECATION] IncomingMessage#mail_from_domain will be removed in + 0.42. It has been replaced by + IncomingMessage#from_email_domain).squish + from_email_domain end def specific_from_name? - !safe_mail_from.nil? && safe_mail_from.strip != info_request.public_body.name.strip + !safe_from_name.nil? && safe_from_name.strip != info_request.public_body.name.strip end def from_public_body? - safe_mail_from.nil? || (mail_from_domain == info_request.public_body.request_email_domain) + safe_from_name.nil? || (from_email_domain == info_request.public_body.request_email_domain) end # This method updates the cached column of the InfoRequest that @@ -517,11 +458,10 @@ def get_main_body_text_part(leaves=[]) end # Returns attachments that are uuencoded in main body part - def _uudecode_and_save_attachments(text) + def _uudecode_attachments(text, start_part_number) # Find any uudecoded things buried in it, yeuchly uus = text.scan(/^begin.+^`\n^end\n/m) - attachments = [] - uus.each do |uu| + uus.map.with_index do |uu, index| # Decode the string content = uu.sub(/\Abegin \d+ [^\n]*\n/, '').unpack('u').first # Make attachment type from it, working out filename and mime type @@ -534,14 +474,15 @@ def _uudecode_and_save_attachments(text) content_type = 'application/octet-stream' end hexdigest = Digest::MD5.hexdigest(content) - attachment = foi_attachments.find_or_create_by(:hexdigest => hexdigest) - attachment.update(:filename => filename, - :content_type => content_type, - :body => content) - attachment.save! - attachments << attachment + attachment = foi_attachments.find_or_initialize_by(hexdigest: hexdigest) + attachment.attributes = { + filename: filename, + content_type: content_type, + body: content, + url_part_number: start_part_number + index + 1 + } + attachment end - attachments end def get_attachments_for_display @@ -556,48 +497,41 @@ def get_attachments_for_display end def extract_attachments! - force = true + extract_attachments + save! + end + + def extract_attachments _mail = raw_email.mail! attachment_attributes = MailHandler.get_attachment_attributes(_mail) - attachments = [] - attachment_attributes.each do |attrs| - attachment = self.foi_attachments.find_or_create_by(:hexdigest => attrs[:hexdigest]) - attachment.update(attrs) - attachment.save! - attachments << attachment + attachment_attributes = attachment_attributes.inject({}) do |memo, attrs| + memo[attrs[:hexdigest]] = attrs + memo end - # Reload to refresh newly created foi_attachments - self.reload + attachments = attachment_attributes.map do |hexdigest, attrs| + attachment = foi_attachments.find_or_initialize_by(hexdigest: hexdigest) + attachment.attributes = attrs + attachment + end - # get the main body part from the set of attachments we just created, - # not from the self.foi_attachments association - some of the total set - # of self.foi_attachments may now be obsolete. Sometimes (e.g. when - # parsing mail from Apple Mail) we can end up with less attachments - # because the hexdigest of an attachment is identical. + # Get the main body part from the set of attachments not from the + # foi_attachments association - some of the total set of foi_attachments may + # now be obsolete. Sometimes (e.g. when parsing mail from Apple Mail) we can + # end up with less attachments because the hexdigest of an attachment is + # identical. main_part = get_main_body_text_part(attachments) - # we don't use get_main_body_text_internal, as we want to avoid charset - # conversions, since _uudecode_and_save_attachments needs to deal with those. + + # We don't use get_main_body_text_internal, as we want to avoid charset + # conversions, since _uudecode_attachments needs to deal with those. # e.g. for https://secure.mysociety.org/admin/foi/request/show_raw_email/24550 - if !main_part.nil? - uudecoded_attachments = _uudecode_and_save_attachments(main_part.body) + if main_part c = _mail.count_first_uudecode_count - for uudecode_attachment in uudecoded_attachments - c += 1 - uudecode_attachment.url_part_number = c - uudecode_attachment.save! - attachments << uudecode_attachment - end + attachments += _uudecode_attachments(main_part.body, c) end - attachment_ids = attachments.map { |attachment| attachment.id } - # now get rid of any attachments we no longer have - FoiAttachment. - where( - ["id NOT IN (?) AND incoming_message_id = ?", - attachment_ids, - self.id] - ).destroy_all + # Purge old attachments that have been rebuilt with a new hexdigest + (foi_attachments - attachments).each(&:mark_for_destruction) end # Returns body text as HTML with quotes flattened, and emails removed. @@ -738,10 +672,6 @@ def get_present_file_extensions end return ret.keys.join(" ") end - # Return space separated list of all file extensions known - def self.get_all_file_extensions - return AlaveteliFileTypes.all_extensions.join(" ") - end def refusals legislation_references.select(&:refusal?).map(&:parent).uniq(&:to_s) diff --git a/app/models/incoming_message/cache_attributes_from_raw_email.rb b/app/models/incoming_message/cache_attributes_from_raw_email.rb new file mode 100644 index 0000000000..cfdc421540 --- /dev/null +++ b/app/models/incoming_message/cache_attributes_from_raw_email.rb @@ -0,0 +1,18 @@ +# Cache attributes from the associated RawEmail before calling the attribute's +# accessor +module IncomingMessage::CacheAttributesFromRawEmail + extend ActiveSupport::Concern + + class_methods do + def cache_from_raw_email(*attrs) + attrs.each { |attr| cache_attribute_from_raw_email(attr) } + end + + def cache_attribute_from_raw_email(attr) + define_method(attr) do + parse_raw_email! + super() + end + end + end +end diff --git a/app/models/info_request.rb b/app/models/info_request.rb index ffdc7cc6b6..9225a1d558 100644 --- a/app/models/info_request.rb +++ b/app/models/info_request.rb @@ -1,5 +1,5 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20220210114052 # # Table name: info_requests # @@ -10,7 +10,7 @@ # created_at :datetime not null # updated_at :datetime not null # described_state :string not null -# awaiting_description :boolean default("false"), not null +# awaiting_description :boolean default(FALSE), not null # prominence :string default("normal"), not null # url_title :text not null # law_used :string default("foi"), not null @@ -19,19 +19,19 @@ # idhash :string not null # external_user_name :string # external_url :string -# attention_requested :boolean default("false") -# comments_allowed :boolean default("true"), not null +# attention_requested :boolean default(FALSE) +# comments_allowed :boolean default(TRUE), not null # info_request_batch_id :integer # last_public_response_at :datetime -# reject_incoming_at_mta :boolean default("false"), not null -# rejected_incoming_count :integer default("0") +# reject_incoming_at_mta :boolean default(FALSE), not null +# rejected_incoming_count :integer default(0) # date_initial_request_last_sent_at :date # date_response_required_by :date # date_very_overdue_after :date # last_event_forming_initial_request_id :integer # use_notifications :boolean # last_event_time :datetime -# incoming_messages_count :integer default("0") +# incoming_messages_count :integer default(0) # public_token :string # @@ -49,6 +49,7 @@ class InfoRequest < ApplicationRecord include InfoRequest::PublicToken include InfoRequest::Sluggable include InfoRequest::TitleValidation + include Taggable @non_admin_columns = %w(title url_title) @additional_admin_columns = %w(rejected_incoming_count) @@ -73,22 +74,22 @@ class InfoRequest < ApplicationRecord :unless => Proc.new { |info_request| info_request.is_batch_request_template? } has_many :info_request_events, - -> { order('created_at, id') }, + -> { order(:created_at, :id) }, :inverse_of => :info_request, :dependent => :destroy has_many :outgoing_messages, - -> { order('created_at') }, + -> { order(:created_at) }, :inverse_of => :info_request, :dependent => :destroy has_many :incoming_messages, - -> { order('created_at') }, + -> { order(:created_at) }, :inverse_of => :info_request, :dependent => :destroy has_many :user_info_request_sent_alerts, :inverse_of => :info_request, :dependent => :destroy has_many :track_things, - -> { order('created_at desc') }, + -> { order(created_at: :desc) }, :inverse_of => :info_request, :dependent => :destroy has_many :widget_votes, @@ -100,11 +101,11 @@ class InfoRequest < ApplicationRecord inverse_of: :citable, dependent: :destroy has_many :comments, - -> { order('created_at') }, + -> { order(:created_at) }, :inverse_of => :info_request, :dependent => :destroy has_many :censor_rules, - -> { order('created_at desc') }, + -> { order(created_at: :desc) }, :inverse_of => :info_request, :dependent => :destroy has_many :mail_server_logs, @@ -129,8 +130,6 @@ class InfoRequest < ApplicationRecord attr_accessor :is_batch_request_template attr_reader :followup_bad_reason - has_tag_string - scope :internal, -> { where.not(user_id: nil) } scope :external, -> { where(user_id: nil) } @@ -631,7 +630,7 @@ def self.recent_requests def self.find_in_state(state) where(:described_state => state). - order('last_event_time') + order(:last_event_time) end def self.log_overdue_events @@ -774,9 +773,7 @@ def user_json_for_api end def reindex_request_events - info_request_events.find_each do |event| - event.xapian_mark_needs_index - end + info_request_events.find_each(&:xapian_mark_needs_index) end # Force reindex when tag string changes @@ -1208,14 +1205,14 @@ def calculate_date_very_overdue_after def last_embargo_set_event info_request_events. where(:event_type => 'set_embargo'). - reorder('created_at DESC'). + reorder(created_at: :desc). first end def last_embargo_expire_event info_request_events. where(:event_type => 'expire_embargo'). - reorder('created_at DESC'). + reorder(created_at: :desc). first end @@ -1448,7 +1445,7 @@ def who_can_followup_to(skip_message = nil) if incoming_message == skip_message next end - incoming_message.safe_mail_from + incoming_message.safe_from_name next if ! incoming_message.is_public? @@ -1761,12 +1758,10 @@ def receive_mail_from_source?(source) true elsif feature_enabled?(:accept_mail_from_anywhere) true + elsif user.features.enabled?(:accept_mail_from_poller) + source == :poller else - if feature_enabled?(:accept_mail_from_poller, user) - source == :poller - else - source == :mailin - end + source == :mailin end end @@ -1866,7 +1861,8 @@ def set_law_used def set_use_notifications if use_notifications.nil? - self.use_notifications = feature_enabled?(:notifications, user) && \ + self.use_notifications = user && + user.features.enabled?(:notifications) && \ info_request_batch_id.present? end return true diff --git a/app/models/info_request_batch.rb b/app/models/info_request_batch.rb index 022986b321..b78a7fc18a 100644 --- a/app/models/info_request_batch.rb +++ b/app/models/info_request_batch.rb @@ -36,8 +36,11 @@ class InfoRequestBatch < ApplicationRecord end }, :inverse_of => :info_request_batches + attr_accessor :ignore_existing_batch + validates_presence_of :user validates_presence_of :body + validates_absence_of :existing_batch, unless: -> { ignore_existing_batch } strip_attributes only: %i[embargo_duration] @@ -58,18 +61,30 @@ def self.send_batches end end - # When constructing a new batch, use this to check user hasn't double submitted. - def self.find_existing(user, title, body, public_body_ids) + def self.with_body(body) + where("regexp_replace(info_request_batches.body, '[[:space:]]', '', 'g') = + regexp_replace(?, '[[:space:]]', '', 'g')", body) + end + + # When constructing a new batch, use this to check user hasn't double + # submitted. + def self.find_existing(user, title, body, public_body_ids, id: nil) conditions = { - :user_id => user, - :title => title, - :body => body, - :info_request_batches_public_bodies => { - :public_body_id => public_body_ids + user_id: user, + title: title, + info_request_batches_public_bodies: { + public_body_id: public_body_ids } } - includes(:public_bodies).where(conditions).references(:public_bodies).first + scope = includes(:public_bodies). + where(conditions). + with_body(body). + references(:public_bodies) + + scope = scope.where.not(id: id) if id + + scope.first end # Create a new batch from the supplied draft version @@ -81,6 +96,10 @@ def self.from_draft(draft) :embargo_duration => draft.embargo_duration) end + def existing_batch + self.class.find_existing(user, title, body, public_body_ids, id: id) + end + # Create a batch of information requests and sends them to public bodies def create_batch! requestable_public_bodies.each do |public_body| @@ -93,7 +112,7 @@ def create_batch! # Sleep between requests in production, in case we're sending a huge # batch which may result in a torrent of auto-replies coming back to # us and overloading the server. - uses_poller = feature_enabled?(:accept_mail_from_poller, user) + uses_poller = user.features.enabled?(:accept_mail_from_poller) sleep 60 if Rails.env.production? && !uses_poller end reload @@ -287,4 +306,8 @@ def is_owning_user?(user) return false unless user user.id == user_id || user.owns_every_request? end + + def prominence + 'normal' + end end diff --git a/app/models/mail_server_log/exim_delivery_status/translated_constants.rb b/app/models/mail_server_log/exim_delivery_status/translated_constants.rb deleted file mode 100644 index 7f537281c8..0000000000 --- a/app/models/mail_server_log/exim_delivery_status/translated_constants.rb +++ /dev/null @@ -1,14 +0,0 @@ -# -*- SkipSchemaAnnotations -class MailServerLog::EximDeliveryStatus - module TranslatedConstants - - def self.humanized - { - :delivered => _('This message has been delivered.'), - :failed => _('This message could not be delivered.'), - :sent => _('This message has been sent.') - }.freeze - end - - end -end diff --git a/app/models/mail_server_log/postfix_delivery_status/translated_constants.rb b/app/models/mail_server_log/postfix_delivery_status/translated_constants.rb deleted file mode 100644 index 81aef8a23b..0000000000 --- a/app/models/mail_server_log/postfix_delivery_status/translated_constants.rb +++ /dev/null @@ -1,14 +0,0 @@ -# -*- SkipSchemaAnnotations -class MailServerLog::PostfixDeliveryStatus - module TranslatedConstants - - def self.humanized - { - :delivered => _('This message has been delivered.'), - :failed => _('This message could not be delivered.'), - :sent => _('This message has been sent.') - }.freeze - end - - end -end diff --git a/app/models/notification.rb b/app/models/notification.rb index a2f132d050..dc3cc7d024 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,17 +1,17 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20220210114052 # # Table name: notifications # # id :integer not null, primary key # info_request_event_id :integer not null # user_id :integer not null -# frequency :integer default("0"), not null +# frequency :integer default("instantly"), not null # seen_at :datetime # send_after :datetime not null # created_at :datetime not null # updated_at :datetime not null -# expired :boolean default("false") +# expired :boolean default(FALSE) # class Notification < ApplicationRecord diff --git a/app/models/outgoing_message.rb b/app/models/outgoing_message.rb index 8c007d7e8b..a31a56d7f0 100644 --- a/app/models/outgoing_message.rb +++ b/app/models/outgoing_message.rb @@ -61,6 +61,8 @@ class OutgoingMessage < ApplicationRecord :inverse_of => :outgoing_message, :dependent => :destroy + delegate :public_body, to: :info_request, private: true, allow_nil: true + after_initialize :set_default_letter # reindex if body text is edited (e.g. by admin interface) after_update :xapian_reindex_after_update @@ -141,8 +143,8 @@ def from # Returns a String def to if replying_to_incoming_message? - # calling safe_mail_from from so censor rules are run - MailHandler.address_from_name_and_email(incoming_message_followup.safe_mail_from, + # calling safe_from_name from so censor rules are run + MailHandler.address_from_name_and_email(incoming_message_followup.safe_from_name, incoming_message_followup.from_email) else info_request.recipient_name_and_email @@ -260,7 +262,7 @@ def sendable? # Returns an Array def smtp_message_ids info_request_events. - order('created_at ASC'). + order(:created_at). map { |event| event.params[:smtp_message_id] }. compact. map do |smtp_id| @@ -342,8 +344,10 @@ def get_text_for_indexing(strip_salutation = true, opts = {}) text = body(opts).strip end - # Remove salutation - text.sub!(/Dear .+,/, "") if strip_salutation + if strip_salutation && public_body + salutation = self.class.default_salutation(public_body) + text.sub!(/#{Regexp.escape(salutation)}\s*/, '') + end # Remove email addresses from display/index etc. self.remove_privacy_sensitive_things!(text) @@ -427,7 +431,7 @@ def default_message_replacements OutgoingMailer. name_for_followup(info_request, incoming_message_followup) else - info_request.try(:public_body).try(:name) + public_body&.name end opts[:letter] = default_letter if default_letter @@ -438,7 +442,7 @@ def default_message_replacements def replying_to_incoming_message? message_type == 'followup' && incoming_message_followup && - incoming_message_followup.safe_mail_from && + incoming_message_followup.safe_from_name && incoming_message_followup.valid_to_reply_to? end diff --git a/app/models/outgoing_message/snippet.rb b/app/models/outgoing_message/snippet.rb index 82a26492bf..40215a9d8b 100644 --- a/app/models/outgoing_message/snippet.rb +++ b/app/models/outgoing_message/snippet.rb @@ -1,14 +1,13 @@ # == Schema Information -# Schema version: 20210928115500 +# Schema version: 20220210114052 # # Table name: outgoing_message_snippets # -# id :bigint not null, primary key -# created_at :datetime not null -# updated_at :datetime not null -# outgoing_message_snippet_id :bigint not null -# name :string -# body :text +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# name :string +# body :text # ## diff --git a/app/models/profile_photo.rb b/app/models/profile_photo.rb index c8ef69525e..65559da8c7 100644 --- a/app/models/profile_photo.rb +++ b/app/models/profile_photo.rb @@ -1,12 +1,12 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20220210114052 # # Table name: profile_photos # # id :integer not null, primary key # data :binary not null # user_id :integer -# draft :boolean default("false"), not null +# draft :boolean default(FALSE), not null # created_at :datetime # updated_at :datetime # diff --git a/app/models/public_body.rb b/app/models/public_body.rb index 04363b4503..d11e58fd71 100644 --- a/app/models/public_body.rb +++ b/app/models/public_body.rb @@ -1,5 +1,5 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20220210114052 # # Table name: public_bodies # @@ -11,14 +11,13 @@ # updated_at :datetime not null # home_page :text # api_key :string not null -# info_requests_count :integer default("0"), not null +# info_requests_count :integer default(0), not null # disclosure_log :text # info_requests_successful_count :integer # info_requests_not_held_count :integer # info_requests_overdue_count :integer # info_requests_visible_classified_count :integer -# info_requests_visible_count :integer default("0"), not null -# public_body_id :integer not null +# info_requests_visible_count :integer default(0), not null # name :text # short_name :text # request_email :text @@ -60,26 +59,26 @@ class ImportCSVDryRun < StandardError; end end has_many :info_requests, - -> { order('created_at desc') }, + -> { order(created_at: :desc) }, :inverse_of => :public_body has_many :track_things, - -> { order('created_at desc') }, + -> { order(created_at: :desc) }, :inverse_of => :public_body, :dependent => :destroy has_many :censor_rules, - -> { order('created_at desc') }, + -> { order(created_at: :desc) }, :inverse_of => :public_body, :dependent => :destroy has_many :track_things_sent_emails, - -> { order('created_at DESC') }, + -> { order(created_at: :desc) }, :inverse_of => :public_body, :dependent => :destroy has_many :public_body_change_requests, - -> { order('created_at DESC') }, + -> { order(created_at: :desc) }, :inverse_of => :public_body, :dependent => :destroy has_many :draft_info_requests, - -> { order('created_at DESC') }, + -> { order(created_at: :desc) }, :inverse_of => :public_body has_and_belongs_to_many :info_request_batches, @@ -169,7 +168,7 @@ class Translation # We could add an `extend_version_class` option pretty trivially by # following the pattern for the existing `extend` option. # - # [1] http://git.io/vIetK + # [1] https://github.com/technoweenie/acts_as_versioned/blob/63b1fc8529/lib/acts_as_versioned.rb#L98-L118 class Version before_save :copy_translated_attributes @@ -217,6 +216,10 @@ def compare(previous = nil, &block) end changes end + + def editor + User.find_by(url_name: last_edit_editor) + end end # Public: Search for Public Bodies whose name, short_name, request_email or @@ -236,7 +239,7 @@ def self.search(query, locale = AlaveteliLocalization.locale) OR lower(has_tag_string_tags.name) like lower('%'||?||'%' ) ) AND has_tag_string_tags.model_id = public_bodies.id - AND has_tag_string_tags.model = 'PublicBody' + AND has_tag_string_tags.model_type = 'PublicBody' AND (public_body_translations.locale = ?) SQL @@ -251,7 +254,7 @@ def self.with_domain(domain) with_translations(AlaveteliLocalization.locale). where("lower(public_body_translations.request_email) " \ "like lower('%'||?||'%')", domain). - order('public_body_translations.name') + merge(PublicBody::Translation.order(:name)) end def set_api_key @@ -583,14 +586,9 @@ def import_values_from_csv_row(row, line, name, options) begin save! rescue ActiveRecord::RecordInvalid - if rails_upgrade? - errors.each do |error| - options[:errors].push "error: line #{ line }: #{ error.full_message } for authority '#{ name }'" - end - else - errors.full_messages.each do |msg| - options[:errors].push "error: line #{ line }: #{ msg } for authority '#{ name }'" - end + errors.each do |error| + options[:errors].push "error: line #{ line }: " \ + "#{ error.full_message } for authority '#{ name }'" end next end @@ -724,7 +722,7 @@ def self.where_clause_for_stats(minimum_requests, total_column) # exclude any that are tagged with 'test' - we use a # sub-select to find the IDs of those public bodies. test_tagged_query = "SELECT model_id FROM has_tag_string_tags" \ - " WHERE model = 'PublicBody' AND name = 'test'" + " WHERE model_type = 'PublicBody' AND name = 'test'" "#{total_column} >= #{minimum_requests} " \ "AND id NOT IN (#{test_tagged_query})" end @@ -813,7 +811,7 @@ def self.popular_bodies(locale) bodies = visible. where('public_body_translations.locale = ?', underscore_locale). - order("info_requests_visible_count desc"). + order(info_requests_visible_count: :desc). limit(32). joins(:translations) else @@ -877,7 +875,7 @@ def self.with_query(query, tag) where("(#{get_public_body_list_translated_condition('current_locale', has_first_letter)}) OR " \ "(#{get_public_body_list_translated_condition('default_locale', has_first_letter)}) ", where_parameters). where('COALESCE(current_locale.name, default_locale.name) IS NOT NULL'). - order('display_name') + order(:display_name) else # The simpler case where we're just searching in the current locale: where_condition = get_public_body_list_translated_condition('public_body_translations', has_first_letter, true) @@ -889,7 +887,7 @@ def self.with_query(query, tag) else where(where_condition, where_parameters). joins(:translations). - order('public_body_translations.name') + merge(PublicBody::Translation.order(:name)) end end end diff --git a/app/models/public_body_category.rb b/app/models/public_body_category.rb index 059e0548cc..b162a53e14 100644 --- a/app/models/public_body_category.rb +++ b/app/models/public_body_category.rb @@ -1,46 +1,55 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20220210114052 # # Table name: public_body_categories # -# id :integer not null, primary key -# category_tag :text not null -# created_at :datetime -# updated_at :datetime -# public_body_category_id :integer not null -# title :text -# description :text +# id :integer not null, primary key +# category_tag :text not null +# created_at :datetime +# updated_at :datetime +# title :text +# description :text # class PublicBodyCategory < ApplicationRecord has_many :public_body_category_links, - :inverse_of => :public_body_category, - :dependent => :destroy + inverse_of: :public_body_category, + dependent: :destroy + has_many :public_body_headings, - :through => :public_body_category_links + through: :public_body_category_links + + has_many :tags, + foreign_key: :name, + primary_key: :category_tag, + class_name: 'HasTagString::HasTagStringTag' + + has_many :public_bodies, + through: :tags, + source: :model, + source_type: 'PublicBody' translates :title, :description - validates_uniqueness_of :category_tag, :message => 'Tag is already taken' - validates_presence_of :title, :message => "Title can't be blank" - validates_presence_of :category_tag, :message => "Tag can't be blank" - validates_presence_of :description, :message => "Description can't be blank" + validates_uniqueness_of :category_tag, message: 'Tag is already taken' + validates_presence_of :title, message: "Title can't be blank" + validates_presence_of :category_tag, message: "Tag can't be blank" + validates_presence_of :description, message: "Description can't be blank" include Translatable def self.get - locale = AlaveteliLocalization.locale || default_locale || "" + locale = AlaveteliLocalization.locale || default_locale || '' categories = CategoryCollection.new + AlaveteliLocalization.with_locale(locale) do - headings = PublicBodyHeading.by_display_order - headings.each do |heading| + PublicBodyHeading.by_display_order.each do |heading| categories << heading.name + heading.public_body_categories.each do |category| - categories << [ - category.category_tag, - category.title, - category.description - ] + categories << [category.category_tag, + category.title, + category.description] end end end @@ -48,19 +57,23 @@ def self.get end def self.without_headings - sql = %Q| SELECT * FROM public_body_categories pbc - WHERE pbc.id NOT IN ( - SELECT public_body_category_id AS id - FROM public_body_category_links - ) | - PublicBodyCategory.find_by_sql(sql) + PublicBodyCategory.find_by_sql(<<~SQL) + SELECT * FROM public_body_categories pbc + WHERE pbc.id NOT IN ( + SELECT public_body_category_id AS id + FROM public_body_category_links + ) + SQL end end PublicBodyCategory::Translation.class_eval do - with_options :if => lambda { |t| !t.default_locale? && t.required_attribute_submitted? } do |required| - required.validates :title, :presence => { :message => "Title can't be blank" } - required.validates :description, :presence => { :message => "Description can't be blank" } + with_options if: ->(t) { + !t.default_locale? && t.required_attribute_submitted? + } do |required| + required.validates :title, presence: { message: "Title can't be blank" } + required.validates :description, + presence: { message: "Description can't be blank" } end def default_locale? @@ -72,5 +85,4 @@ def required_attribute_submitted? !read_attribute(attribute).blank? end end - end diff --git a/app/models/public_body_category_link.rb b/app/models/public_body_category_link.rb index 500f067092..8a6b799bcb 100644 --- a/app/models/public_body_category_link.rb +++ b/app/models/public_body_category_link.rb @@ -13,27 +13,53 @@ class PublicBodyCategoryLink < ApplicationRecord belongs_to :public_body_category, - :inverse_of => :public_body_category_links + inverse_of: :public_body_category_links + belongs_to :public_body_heading, - :inverse_of => :public_body_category_links + inverse_of: :public_body_category_links validates_presence_of :public_body_category validates_presence_of :public_body_heading - validates :category_display_order, :numericality => { :only_integer => true, - :message => 'Display order must be a number' } - before_validation :on => :create do - unless self.category_display_order - self.category_display_order = PublicBodyCategoryLink.next_display_order(public_body_heading_id) - end + validates :category_display_order, numericality: { + only_integer: true, message: 'Display order must be a number' + } + + before_validation on: :create do + self.category_display_order ||= + self.class.next_display_order(public_body_heading) + end + + scope :for_heading, ->(public_body_heading) do + where(public_body_heading: public_body_heading). + order(:category_display_order) end - def self.next_display_order(heading_id) - if last = where(:public_body_heading_id => heading_id).order(:category_display_order).last - last.category_display_order + 1 + def self.next_display_order(public_body_heading) + last_record = for_heading(public_body_heading).last + + if last_record + last_record.category_display_order + 1 else 0 end end + def self.by_display_order + headings_table = Arel::Table.new(:public_body_headings) + links_table = Arel::Table.new(:public_body_category_links) + + PublicBodyCategoryLink. + distinct. + select(headings_table[:display_order], links_table[Arel.star]). + joins(:public_body_heading). + merge(PublicBodyHeading.by_display_order). + joins(public_body_category: :public_bodies). + merge(PublicBody.is_requestable). + order(:category_display_order). + preload( + public_body_heading: :translations, + public_body_category: :translations + ) + end end diff --git a/app/models/public_body_change_request.rb b/app/models/public_body_change_request.rb index 378f0c056a..ff652a104a 100644 --- a/app/models/public_body_change_request.rb +++ b/app/models/public_body_change_request.rb @@ -1,5 +1,5 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20220210114052 # # Table name: public_body_change_requests # @@ -12,7 +12,7 @@ # public_body_email :string # source_url :text # notes :text -# is_open :boolean default("true"), not null +# is_open :boolean default(TRUE), not null # created_at :datetime not null # updated_at :datetime not null # @@ -37,10 +37,10 @@ class PublicBodyChangeRequest < ApplicationRecord validate :body_email_format, :unless => proc { |change_request| change_request.public_body_email.blank? } scope :new_body_requests, -> { - where(public_body_id: nil).order("created_at") + where(public_body_id: nil).order(:created_at) } scope :body_update_requests, -> { - where("public_body_id IS NOT NULL").order("created_at") + where("public_body_id IS NOT NULL").order(:created_at) } singleton_class.undef_method :open # Undefine Kernel.open to avoid warning diff --git a/app/models/public_body_heading.rb b/app/models/public_body_heading.rb index 71d9d95b38..2ba476523b 100644 --- a/app/models/public_body_heading.rb +++ b/app/models/public_body_heading.rb @@ -1,35 +1,37 @@ # == Schema Information -# Schema version: 20210114161442 +# Schema version: 20220210114052 # # Table name: public_body_headings # -# id :integer not null, primary key -# display_order :integer -# created_at :datetime -# updated_at :datetime -# public_body_heading_id :integer not null -# name :text +# id :integer not null, primary key +# display_order :integer +# created_at :datetime +# updated_at :datetime +# name :text # class PublicBodyHeading < ApplicationRecord has_many :public_body_category_links, - :inverse_of => :public_body_heading, - :dependent => :destroy + inverse_of: :public_body_heading, + dependent: :destroy + has_many :public_body_categories, - -> { order('public_body_category_links.category_display_order') }, - :through => :public_body_category_links + -> { merge(PublicBodyCategoryLink.order(:category_display_order)) }, + through: :public_body_category_links - scope :by_display_order, -> { order('display_order ASC') } + scope :by_display_order, -> { order(:display_order) } translates :name - validates_uniqueness_of :name, :message => 'Name is already taken' - validates_presence_of :name, :message => 'Name can\'t be blank' - validates :display_order, :numericality => { :only_integer => true, - :message => 'Display order must be a number' } + validates_uniqueness_of :name, message: 'Name is already taken' + validates_presence_of :name, message: "Name can't be blank" + + validates :display_order, numericality: { + only_integer: true, message: 'Display order must be a number' + } - before_validation :on => :create do - unless self.display_order + before_validation on: :create do + unless display_order self.display_order = PublicBodyHeading.next_display_order end end diff --git a/app/models/raw_email.rb b/app/models/raw_email.rb index b329aa9012..cb14dbbe41 100644 --- a/app/models/raw_email.rb +++ b/app/models/raw_email.rb @@ -20,6 +20,8 @@ class RawEmail < ApplicationRecord has_one :incoming_message, :inverse_of => :raw_email + has_one_attached :file, service: :raw_emails + delegate :date, to: :mail delegate :message_id, to: :mail delegate :multipart?, to: :mail @@ -66,6 +68,15 @@ def empty_from_field? end def directory + if file.attached? + warn <<~DEPRECATION.squish + [DEPRECATION] RawEmail#directory shouldn't be used when using + `ActiveStorage` backed file stores. This method will be removed + in 0.42. + DEPRECATION + return + end + if request_id.empty? raise "Failed to find the id number of the associated request: has it been saved?" end @@ -79,6 +90,15 @@ def directory end def filepath + if file.attached? + warn <<~DEPRECATION.squish + [DEPRECATION] RawEmail#filepath shouldn't be used when using + `ActiveStorage` backed file stores. This method will be removed + in 0.42. + DEPRECATION + return + end + if incoming_message_id.empty? raise "Failed to find the id number of the associated incoming message: has it been saved?" end @@ -95,14 +115,17 @@ def mail! end def data=(d) - FileUtils.mkdir_p(directory) unless File.exist?(directory) - File.atomic_write(filepath) do |file| - file.binmode - file.write(d) - end + @data = d.to_s + file.attach( + io: StringIO.new(@data), + filename: "#{incoming_message_id}.eml", + content_type: 'message/rfc822' + ) end def data + return @data ||= file.download if file.attached? + File.open(filepath, "rb").read end @@ -113,7 +136,11 @@ def data_as_text end def destroy_file_representation! - File.delete(filepath) if File.exist?(filepath) + if file.attached? + file.purge + elsif File.exist?(filepath) + File.delete(filepath) + end end def from_name diff --git a/app/models/request_classification.rb b/app/models/request_classification.rb index bff373777c..d6e9162109 100644 --- a/app/models/request_classification.rb +++ b/app/models/request_classification.rb @@ -23,7 +23,7 @@ class RequestClassification < ApplicationRecord def self.league_table(size, conditions=nil) query = select('user_id, count(*) as cnt'). group('user_id'). - order('cnt desc'). + order(cnt: :desc). limit(size). includes(:user) query = query.where(*conditions) if conditions diff --git a/app/models/statistics/leaderboard.rb b/app/models/statistics/leaderboard.rb index 2600626898..974e69274a 100644 --- a/app/models/statistics/leaderboard.rb +++ b/app/models/statistics/leaderboard.rb @@ -5,7 +5,7 @@ def all_time_requesters InfoRequest.is_public. joins(:user). group(:user). - order('count_info_requests_all DESC'). + order(count_info_requests_all: :desc). limit(10). count end @@ -16,7 +16,7 @@ def last_28_day_requesters where('info_requests.created_at >= ?', 28.days.ago). joins(:user). group(:user). - order('count_info_requests_all DESC'). + order(count_info_requests_all: :desc). limit(10). count end @@ -25,7 +25,7 @@ def all_time_commenters commenters = Comment.visible. joins(:user). group('comments.user_id'). - order('count_all DESC'). + order(count_all: :desc). limit(10). count # TODO: Have user objects automatically instantiated like the InfoRequest @@ -41,7 +41,7 @@ def last_28_day_commenters where('comments.created_at >= ?', 28.days.ago). joins(:user). group('comments.user_id'). - order('count_all DESC'). + order(count_all: :desc). limit(10). count # TODO: Have user objects automatically instantiated like the InfoRequest diff --git a/app/models/user.rb b/app/models/user.rb index ae711e177f..0729af6acf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,5 @@ # == Schema Information -# Schema version: 20210921094059 +# Schema version: 20220210114052 # # Table name: users # @@ -10,27 +10,27 @@ # salt :string # created_at :datetime not null # updated_at :datetime not null -# email_confirmed :boolean default("false"), not null +# email_confirmed :boolean default(FALSE), not null # url_name :text not null -# last_daily_track_email :datetime default("2000-01-01 00:00:00") +# last_daily_track_email :datetime default(Sat, 01 Jan 2000 00:00:00.000000000 GMT +00:00) # ban_text :text default(""), not null # about_me :text default(""), not null # locale :string # email_bounced_at :datetime # email_bounce_message :text default(""), not null -# no_limit :boolean default("false"), not null -# receive_email_alerts :boolean default("true"), not null -# can_make_batch_requests :boolean default("false"), not null -# otp_enabled :boolean default("false"), not null +# no_limit :boolean default(FALSE), not null +# receive_email_alerts :boolean default(TRUE), not null +# can_make_batch_requests :boolean default(FALSE), not null +# otp_enabled :boolean default(FALSE), not null # otp_secret_key :string -# otp_counter :integer default("1") -# confirmed_not_spam :boolean default("false"), not null -# comments_count :integer default("0"), not null -# info_requests_count :integer default("0"), not null -# track_things_count :integer default("0"), not null -# request_classifications_count :integer default("0"), not null -# public_body_change_requests_count :integer default("0"), not null -# info_request_batches_count :integer default("0"), not null +# otp_counter :integer default(1) +# confirmed_not_spam :boolean default(FALSE), not null +# comments_count :integer default(0), not null +# info_requests_count :integer default(0), not null +# track_things_count :integer default(0), not null +# request_classifications_count :integer default(0), not null +# public_body_change_requests_count :integer default(0), not null +# info_request_batches_count :integer default(0), not null # daily_summary_hour :integer # daily_summary_minute :integer # closed_at :datetime @@ -45,123 +45,142 @@ class User < ApplicationRecord include User::OneTimePassword include User::Survey - rolify before_add: :setup_pro_account - strip_attributes :allow_empty => true + CONTENT_LIMIT = { + info_requests: AlaveteliConfiguration.max_requests_per_user_per_day, + comments: AlaveteliConfiguration.max_requests_per_user_per_day + }.freeze + + rolify before_add: :setup_pro_account, + after_add: :assign_role_features, + after_remove: :assign_role_features + strip_attributes allow_empty: true attr_accessor :no_xapian_reindex has_many :info_requests, - -> { order('info_requests.created_at desc') }, - :inverse_of => :user, - :dependent => :destroy + -> { order(created_at: :desc) }, + inverse_of: :user, + dependent: :destroy has_many :info_request_events, - -> { reorder('created_at desc') }, - :through => :info_requests + -> { reorder(created_at: :desc) }, + through: :info_requests has_many :embargoes, - :inverse_of => :user, - :through => :info_requests + inverse_of: :user, + through: :info_requests has_many :draft_info_requests, - -> { order('created_at desc') }, - :inverse_of => :user, - :dependent => :destroy + -> { order(created_at: :desc) }, + inverse_of: :user, + dependent: :destroy has_many :user_info_request_sent_alerts, - :inverse_of => :user, - :dependent => :destroy + inverse_of: :user, + dependent: :destroy has_many :post_redirects, - -> { order('created_at desc') }, - :inverse_of => :user, - :dependent => :destroy + -> { order(created_at: :desc) }, + inverse_of: :user, + dependent: :destroy has_many :track_things, - -> { order('created_at desc') }, - :inverse_of => :tracking_user, - :foreign_key => 'tracking_user_id', - :dependent => :destroy + -> { order(created_at: :desc) }, + inverse_of: :tracking_user, + foreign_key: 'tracking_user_id', + dependent: :destroy has_many :citations, - -> { order('created_at desc') }, + -> { order(created_at: :desc) }, inverse_of: :user, dependent: :destroy has_many :comments, - -> { order('created_at desc') }, - :inverse_of => :user, - :dependent => :destroy + -> { order(created_at: :desc) }, + inverse_of: :user, + dependent: :destroy has_many :public_body_change_requests, - -> { order('created_at desc') }, - :inverse_of => :user, - :dependent => :destroy + -> { order(created_at: :desc) }, + inverse_of: :user, + dependent: :destroy has_one :profile_photo, - :inverse_of => :user, - :dependent => :destroy + inverse_of: :user, + dependent: :destroy has_many :censor_rules, - -> { order('created_at desc') }, - :inverse_of => :user, - :dependent => :destroy + -> { order(created_at: :desc) }, + inverse_of: :user, + dependent: :destroy has_many :info_request_batches, - -> { order('created_at desc') }, - :inverse_of => :user, - :dependent => :destroy + -> { order(created_at: :desc) }, + inverse_of: :user, + dependent: :destroy has_many :draft_info_request_batches, - -> { order('created_at desc') }, - :inverse_of => :user, - :dependent => :destroy, - :class_name => 'AlaveteliPro::DraftInfoRequestBatch' + -> { order(created_at: :desc) }, + inverse_of: :user, + dependent: :destroy, + class_name: 'AlaveteliPro::DraftInfoRequestBatch' has_many :request_classifications, - :inverse_of => :user, - :dependent => :destroy + inverse_of: :user, + dependent: :destroy has_one :pro_account, - :inverse_of => :user, - :dependent => :destroy + inverse_of: :user, + dependent: :destroy has_many :request_summaries, - :inverse_of => :user, - :dependent => :destroy, - :class_name => 'AlaveteliPro::RequestSummary' + inverse_of: :user, + dependent: :destroy, + class_name: 'AlaveteliPro::RequestSummary' has_many :notifications, - :inverse_of => :user, - :dependent => :destroy + inverse_of: :user, + dependent: :destroy has_many :track_things_sent_emails, - :inverse_of => :user, - :dependent => :destroy + inverse_of: :user, + dependent: :destroy has_many :track_things_sent_emails, - :dependent => :destroy + dependent: :destroy has_many :announcements, - :inverse_of => :user + inverse_of: :user has_many :announcement_dismissals, - :inverse_of => :user, - :dependent => :destroy + inverse_of: :user, + dependent: :destroy has_many :memberships, class_name: 'Project::Membership' has_many :projects, through: :memberships + has_many :sign_ins, + class_name: 'User::SignIn', + inverse_of: :user, + dependent: :destroy + scope :active, -> { not_banned.not_closed } - scope :banned, -> { where.not(ban_text: "") } - scope :not_banned, -> { where(ban_text: "") } + scope :banned, -> { where.not(ban_text: '') } + scope :not_banned, -> { where(ban_text: '') } scope :closed, -> { where.not(closed_at: nil) } scope :not_closed, -> { where(closed_at: nil) } - validates_presence_of :email, :message => _("Please enter your email address") - validates_presence_of :name, :message => _("Please enter your name") + validates_presence_of :email, message: _('Please enter your email address') + validates_presence_of :name, message: _('Please enter your name') validates_length_of :about_me, - :maximum => 500, - :message => _("Please keep it shorter than 500 characters") + maximum: 500, + message: _('Please keep it shorter than 500 characters') - validates :email, :uniqueness => { - :case_sensitive => false, - :message => _("This email is already in use") } + validates :email, + uniqueness: { case_sensitive: false, + message: _('This email is already in use') } validate :email_and_name_are_valid after_initialize :set_defaults after_update :reindex_referencing_models, :update_pro_account - acts_as_xapian :texts => [ :name, :about_me ], - :values => [ - [ :created_at_numeric, 1, "created_at", :number ] # for sorting - ], - :terms => [ [ :variety, 'V', "variety" ] ], - :if => :indexed_by_search? + acts_as_xapian texts: [:name, :about_me], + values: [ + [:created_at_numeric, 1, 'created_at', :number] # for sorting + ], + terms: [[:variety, 'V', 'variety']], + if: :indexed_by_search? + def self.search(query) + where(<<~SQL, query: query) + lower(users.name) LIKE lower('%'||:query||'%') OR + lower(users.email) LIKE lower('%'||:query||'%') OR + lower(users.about_me) LIKE lower('%'||:query||'%') + SQL + end def self.pro - with_role :pro + with_role(:pro) end # Return user given login email, password and other form parameters (e.g. name) @@ -206,50 +225,23 @@ def self.find_user_by_email(email) # The "internal admin" is a special user for internal use. def self.internal_admin_user - user = User.find_by_email(AlaveteliConfiguration::contact_email) - if user.nil? - password = PostRedirect.generate_random_token - user = User.new( - :name => 'Internal admin user', - :email => AlaveteliConfiguration.contact_email, - :password => password, - :password_confirmation => password - ) - user.save! - end + user = find_by(email: AlaveteliConfiguration.contact_email) + return user if user - user - end - - def self.owns_every_request?(user) - warn %q([DEPRECATION] User#owns_every_request? will be removed in 0.41. - It has been replaced by User#owns_every_request?).squish - user&.owns_every_request? - end - - def self.view_hidden?(user) - warn %q([DEPRECATION] User.view_hidden? will be removed in 0.41. - It has been replaced by User#view_hidden?).squish - user&.view_hidden? - end + password = PostRedirect.generate_random_token - def self.view_embargoed?(user) - warn %q([DEPRECATION] User.view_embargoed? will be removed in 0.41. - It has been replaced by User#view_embargoed?).squish - user&.view_embargoed? - end - - def self.view_hidden_and_embargoed?(user) - warn %q([DEPRECATION] User.view_hidden_and_embargoed? will be removed in - 0.41. It has been replaced by User#view_hidden_and_embargoed?). - squish - user&.view_hidden_and_embargoed? + create!( + name: 'Internal admin user', + email: AlaveteliConfiguration.contact_email, + password: password, + password_confirmation: password + ) end # Should the user be kept logged into their own account # if they follow a /c/ redirect link belonging to another user? def self.stay_logged_in_on_redirect?(user) - !user.nil? && user.is_admin? + user&.is_admin? end # Used for default values of last_daily_track_email @@ -268,10 +260,10 @@ def self.random_time_in_last_day # This SQL statement is useful for seeing how spread out users are at the moment: # select extract(hour from last_daily_track_email) as h, count(*) from users group by extract(hour from last_daily_track_email) order by h; def self.spread_alert_times_across_day - self.find_each do |user| - user.last_daily_track_email = User.random_time_in_last_day - user.save! + find_each do |user| + user.update!(last_daily_track_email: User.random_time_in_last_day) end + nil # so doesn't print all users on console end @@ -280,7 +272,7 @@ def self.record_bounce_for_email(email, message) return false if user.nil? user.record_bounce(message) if user.email_bounced_at.nil? - return true + true end def self.find_similar_named_users(user) @@ -312,7 +304,7 @@ def created_at_numeric end def variety - "user" + 'user' end # requested_by: and commented_by: search queries also need updating after save @@ -329,12 +321,7 @@ def expire_requests end def expire_comments - comments.find_each do |comment| - # TODO: Extract to Comment#expire - comment.info_request_events.find_each do |info_request_event| - info_request_event.xapian_mark_needs_index - end - end + comments.find_each(&:reindex_request_events) end def locale @@ -344,7 +331,7 @@ def locale def name _name = read_attribute(:name) if suspended? - _name = _("{{user_name}} (Account suspended)", :user_name => _name) + _name = _('{{user_name}} (Account suspended)', user_name: _name) end _name end @@ -376,10 +363,9 @@ def name_and_email # Returns list of requests which the user hasn't described (and last # changed more than a day ago) def get_undescribed_requests - info_requests.where( - "awaiting_description = ? and #{ InfoRequest.last_event_time_clause } < ?", - true, 1.day.ago - ) + info_requests. + where(awaiting_description: true). + where("#{ InfoRequest.last_event_time_clause } < ?", 1.day.ago) end # Does the user magically gain powers as if they owned every request? @@ -389,7 +375,10 @@ def owns_every_request? end def can_admin_roles - roles.flat_map { |role| Role.grants_and_revokes(role.name.to_sym) }.compact.uniq + roles. + flat_map { |role| Role.grants_and_revokes(role.name.to_sym) }. + compact. + uniq end def can_admin_role?(role) @@ -401,9 +390,12 @@ def admin_page_links? is_admin? end - # Is it public that they are banned? def banned? - !ban_text.empty? + ban_text.present? + end + + def close + update(closed_at: Time.zone.now) end def closed? @@ -413,17 +405,21 @@ def closed? def close_and_anonymise sha = Digest::SHA1.hexdigest(rand.to_s) - redact_name! if info_requests.any? + transaction do + redact_name! if info_requests.any? - update( - name: _('[Name Removed]'), - email: "#{sha}@invalid", - url_name: sha, - about_me: '', - password: MySociety::Util.generate_token, - receive_email_alerts: false, - closed_at: Time.zone.now - ) + sign_ins.destroy_all + + update( + name: _('[Name Removed]'), + email: "#{sha}@invalid", + url_name: sha, + about_me: '', + password: MySociety::Util.generate_token, + receive_email_alerts: false, + closed_at: Time.zone.now + ) + end end def active? @@ -434,27 +430,42 @@ def suspended? !active? end + def prominence + return 'hidden' if banned? + return 'backpage' if closed? + return 'backpage' unless email_confirmed? + 'normal' + end + # Various ways the user can be banned, and text to describe it if failed def can_file_requests? - active? && !exceeded_limit? + active? && !exceeded_limit?(:info_requests) + end + + def can_make_followup? + active? end - def exceeded_limit? - # Some users have no limit - return false if no_limit + def can_make_comments? + active? && !exceeded_limit?(:comments) + end - # Batch request users don't have a limit - return false if can_make_batch_requests? + def can_contact_other_users? + active? + end - # Has the user issued as many as MAX_REQUESTS_PER_USER_PER_DAY requests in the past 24 hours? - return false if AlaveteliConfiguration.max_requests_per_user_per_day.blank? + def exceeded_limit?(content) + return false if no_limit? + return false if can_make_batch_requests? + return false if content_limit(content).blank? - recent_requests = - InfoRequest. + # Has the User created too much of the content in the past 24 hours? + recent_content = + content.to_s.classify.constantize. where(["user_id = ? AND created_at > now() - '1 day'::interval", id]). - count + count - recent_requests >= AlaveteliConfiguration.max_requests_per_user_per_day + recent_content >= content_limit(content) end def next_request_permitted_at @@ -463,7 +474,7 @@ def next_request_permitted_at n_most_recent_requests = InfoRequest. where(["user_id = ? AND created_at > now() - '1 day'::interval", id]). - order('created_at DESC'). + order(created_at: :desc). limit(AlaveteliConfiguration.max_requests_per_user_per_day) return nil if n_most_recent_requests.size < AlaveteliConfiguration::max_requests_per_user_per_day @@ -472,28 +483,16 @@ def next_request_permitted_at nth_most_recent_request.created_at + 1.day end - def can_make_followup? - active? - end - - def can_make_comments? - active? - end - - def can_contact_other_users? - active? - end - def can_fail_html if banned? text = ban_text.strip elsif closed? text = _('Account closed at user request') else - raise "Unknown reason for ban" + raise 'Unknown reason for ban' end text = CGI.escapeHTML(text) - text = MySociety::Format.make_clickable(text, :contract => 1) + text = MySociety::Format.make_clickable(text, contract: 1) text = text.gsub(/\n/, '
') text.html_safe end @@ -518,7 +517,7 @@ def show_profile_photo? def about_me_already_exists? return false if about_me.blank? - self.class.where(:about_me => about_me).where.not(id: id).any? + self.class.where(about_me: about_me).where.not(id: id).any? end # Return about me text for display as HTML @@ -526,27 +525,28 @@ def about_me_already_exists? def get_about_me_for_html_display text = about_me.strip text = CGI.escapeHTML(text) - text = MySociety::Format.make_clickable(text, { :contract => 1, :nofollow => true }) + text = MySociety::Format.make_clickable(text, contract: 1, nofollow: true) text = text.gsub(/\n/, '
') text.html_safe end def json_for_api { - :id => id, - :url_name => url_name, - :name => name, - :ban_text => ban_text, - :about_me => about_me, + id: id, + url_name: url_name, + name: name, + ban_text: ban_text, + about_me: about_me # :profile_photo => self.profile_photo # ought to have this, but too hard to get URL out for now # created_at / updated_at we only show the year on the main page for privacy reasons, so don't put here } end def record_bounce(message) - self.email_bounced_at = Time.zone.now - self.email_bounce_message = convert_string_to_utf8(message).string - save! + update!( + email_bounced_at: Time.zone.now, + email_bounce_message: convert_string_to_utf8(message).string + ) end def confirm(save_record = false) @@ -598,28 +598,36 @@ def next_daily_summary_time end def daily_summary_time - { - hour: self.daily_summary_hour, - min: self.daily_summary_minute - } + { hour: daily_summary_hour, + min: daily_summary_minute } end # With what frequency does the user want to be notified? def notification_frequency - if feature_enabled? :notifications, self + if features.enabled?(:notifications) Notification::DAILY else Notification::INSTANTLY end end + def features + # Will return enabled and disabled features. Call #enabled? to see the + # current state + AlaveteliFeatures.features.with_actor(self) + end + + def features=(new_features) + features.assign_features(new_features) + end + # Define an id number for use with the Flipper gem's user-by-user feature # flagging. We prefix with the class because features can be enabled for # other types of objects (e.g Roles) in the same way and will be stored in # the same table. See: # https://github.com/jnunemaker/flipper/blob/master/docs/Gates.md def flipper_id - return "User;#{id}" + "User;#{id}" end private @@ -632,18 +640,16 @@ def redact_name! end def set_defaults - if new_record? - # make alert emails go out at a random time for each new user, so - # overall they are spread out throughout the day. - self.last_daily_track_email = User.random_time_in_last_day - # Make daily summary emails go out at a random time for each new user - # too, if it's not already set - if self.daily_summary_hour.nil? && self.daily_summary_minute.nil? - random_time = User.random_time_in_last_day - self.daily_summary_hour = random_time.hour - self.daily_summary_minute = random_time.min - end - end + return unless new_record? + + # make alert emails go out at a random time for each new user, so + # overall they are spread out throughout the day. + self.last_daily_track_email = self.class.random_time_in_last_day + + # Make daily summary emails go out at a random time for each new user + # too, if it's not already set + self.daily_summary_hour ||= self.class.random_time_in_last_day.hour + self.daily_summary_minute ||= self.class.random_time_in_last_day.min end def email_and_name_are_valid @@ -655,14 +661,20 @@ def email_and_name_are_valid end end + def assign_role_features(_role) + features.assign_role_features + end + def setup_pro_account(role) return unless role == Role.pro_role pro_account || build_pro_account if feature_enabled?(:pro_pricing) - AlaveteliPro::Access.grant(self) end def update_pro_account pro_account.update_stripe_customer if pro_account end + def content_limit(content) + CONTENT_LIMIT[content] + end end diff --git a/app/models/user/login_token.rb b/app/models/user/login_token.rb index d6824dd15a..9d7d62db86 100644 --- a/app/models/user/login_token.rb +++ b/app/models/user/login_token.rb @@ -2,6 +2,8 @@ module User::LoginToken extend ActiveSupport::Concern + LOGIN_TOKEN_NAMESPACE = 'b14cba73-a392-4de4-a9ed-06d7f0ced429' + included do before_save :set_login_token end @@ -13,9 +15,13 @@ def set_login_token end def set_login_token! - self.login_token = Digest::UUID.uuid_v5("User;#{id}", { - email: email, - hashed_password: hashed_password - }.to_s) + self.login_token = Digest::UUID.uuid_v5( + LOGIN_TOKEN_NAMESPACE, + { + user: id, + email: email, + hashed_password: hashed_password + }.to_s + ) end end diff --git a/app/models/user/sign_in.rb b/app/models/user/sign_in.rb new file mode 100644 index 0000000000..159741080b --- /dev/null +++ b/app/models/user/sign_in.rb @@ -0,0 +1,54 @@ +# == Schema Information +# Schema version: 20220225214524 +# +# Table name: user_sign_ins +# +# id :bigint not null, primary key +# user_id :bigint +# ip :inet +# created_at :datetime not null +# updated_at :datetime not null +# country :string +# + +# Record medadata about User sign in activity +class User::SignIn < ApplicationRecord + default_scope { order(created_at: :desc) } + + belongs_to :user, inverse_of: :sign_ins + + before_create :create? + + def self.purge + where('created_at < ?', retention_days.days.ago).destroy_all + end + + def self.search(query) + joins(:user).references(:users).where(<<~SQL, query: query) + lower(user_sign_ins.ip::text) LIKE lower('%'||:query||'%') OR + lower(user_sign_ins.country) LIKE lower('%'||:query||'%') OR + lower(users.name) LIKE lower('%'||:query||'%') OR + lower(users.email) LIKE lower('%'||:query||'%') + SQL + end + + def self.retain_signins? + retention_days >= 1 + end + + def self.retention_days + AlaveteliConfiguration.user_sign_in_activity_retention_days + end + + def other_users + User.distinct.joins(:sign_ins). + where(user_sign_ins: { ip: ip }). + where.not(id: user_id) + end + + private + + def create? + throw :abort unless self.class.retain_signins? + end +end diff --git a/app/models/user/transaction_calculator.rb b/app/models/user/transaction_calculator.rb index 2abffe65ca..98fc229539 100644 --- a/app/models/user/transaction_calculator.rb +++ b/app/models/user/transaction_calculator.rb @@ -57,7 +57,7 @@ def total_per_month user. send(assoc). group("DATE_TRUNC('month', created_at)"). - reorder("date_trunc_month_created_at"). + reorder(:date_trunc_month_created_at). count # Add the counts to existing keys, or set new keys if they don't exist diff --git a/app/models/user/with_activity_query.rb b/app/models/user/with_activity_query.rb index 8f0f801e4b..09864a7215 100644 --- a/app/models/user/with_activity_query.rb +++ b/app/models/user/with_activity_query.rb @@ -8,7 +8,7 @@ # # => User::ActiveRecord_Relation # # # ID of most active user: -# q.call.order('activity DESC').first.id +# q.call.order(activity: :desc).first.id # # => 19 # # # Activity in date range: diff --git a/app/services/alaveteli_pro/access.rb b/app/services/alaveteli_pro/access.rb deleted file mode 100644 index fc16e6d708..0000000000 --- a/app/services/alaveteli_pro/access.rb +++ /dev/null @@ -1,32 +0,0 @@ -## -# A service object to grant and revoke users access to Alaveteli Professional -# features -# -# Usage: -# AlaveteliPro::Access.grant(user) -# -# TODO: -# AlaveteliPro::Access.revoke(user) -# -class AlaveteliPro::Access - include AlaveteliFeatures::Helpers - - def self.grant(*args, &block) - new(*args, &block).grant - end - - attr_reader :user - - def initialize(user) - @user = user - end - - def grant - # enable the mail poller only if the POP polling is configured - if AlaveteliConfiguration.production_mailer_retriever_method == 'pop' - enable_actor(:accept_mail_from_poller, user) - end - - enable_actor(:notifications, user) - end -end diff --git a/app/views/admin/citations/_list.html.erb b/app/views/admin/citations/_list.html.erb new file mode 100644 index 0000000000..c594b4ad65 --- /dev/null +++ b/app/views/admin/citations/_list.html.erb @@ -0,0 +1,20 @@ +
+ <% if citations.any? %> + <% citations.each do |citation| %> +
+ + <%= link_to citation.source_url, citation.source_url %> + + + +
+ <% end %> + <% else %> +

None yet.

+ <% end %> +
diff --git a/app/views/admin/users/_sign_in_table.html.erb b/app/views/admin/users/_sign_in_table.html.erb new file mode 100644 index 0000000000..7bc3185d39 --- /dev/null +++ b/app/views/admin/users/_sign_in_table.html.erb @@ -0,0 +1,48 @@ +
+ <% unless User::SignIn.retain_signins? %> +
+

Sign In Retention Disabled

+ To enable sign in retention set + USER_SIGN_IN_ACTIVITY_RETENTION_DAYS to a value greater than + 0 in Alaveteli’s configuration. Any existing records below will + soon be purged and no further sign ins will be tracked. +
+ <% end %> + + <% if sign_ins.any? %> + + <% sign_ins.each do |sign_in| %> + + + + + + + + + + + + <% end %> +
<%= user_both_links(sign_in.user) %> + + <%= link_to sign_in.ip, admin_sign_ins_path(query: sign_in.ip) %> + + + + <% if sign_in.country %> + <%= link_to admin_sign_ins_path(query: sign_in.country) do %> + <%= sign_in.country %> + <% end %> + <% else %> + ?? + <% end %> + + <%= admin_date(sign_in.created_at, ago_only: true) %> + <%= sign_in.other_users.size %> others using this + IP +
+ <% elsif User::SignIn.retain_signins? %> +

None yet.

+ <% end %> +
diff --git a/app/views/admin/users/sign_ins/index.html.erb b/app/views/admin/users/sign_ins/index.html.erb new file mode 100644 index 0000000000..8cb4c23a5b --- /dev/null +++ b/app/views/admin/users/sign_ins/index.html.erb @@ -0,0 +1,19 @@ +<%= render 'admin_user/scopes' %> + +
+ <%= form_tag admin_sign_ins_path, method: :get, class: 'form form-search span12' do %> +
+ <%= text_field_tag 'query', params[:query], size: 30, class: 'input-large search-query' %> + <%= submit_tag 'Search', class: 'btn' %> +
+ + + (substring search: names, emails, country and IP) + + <% end %> +
+ +<%= render partial: 'admin/users/sign_in_table', + locals: { sign_ins: @sign_ins } %> + +<%= will_paginate(@sign_ins, class: 'paginator') %> diff --git a/app/views/admin_censor_rule/_form.html.erb b/app/views/admin_censor_rule/_form.html.erb index 1b81c920cb..603dfe4a8f 100644 --- a/app/views/admin_censor_rule/_form.html.erb +++ b/app/views/admin_censor_rule/_form.html.erb @@ -1,19 +1,7 @@ <%= foi_error_messages_for :censor_rule %>
- Applies to - <% unless info_request.nil? %> - <%= request_both_links(info_request) %> - <% end %> - <% unless user.nil? %> - <%= user_both_links(user) %> - <% end %> - <% unless public_body.nil? %> - <%= public_body_both_links(public_body) %> - <% end %> - <% if info_request.nil? && user.nil? && public_body.nil? %> - everything - <% end %> + Applies to <%= censor_rule_applies_to(@censor_rule) %>
@@ -29,7 +17,7 @@
- <%= text_field 'censor_rule', 'text', :class => "span3" %> + <%= text_field 'censor_rule', 'text', class: 'span6' %>
that you want to remove, case sensitive
@@ -39,9 +27,28 @@
- <%= text_field 'censor_rule', 'replacement', :class => "span3" %> + <% placeholder = + if @censor_rule.canned_replacements.any? + 'Select or add your own' + end + %> + + <%= text_field 'censor_rule', + 'replacement', + class: 'span6', + list: 'canned-replacements', + autocomplete: 'off', + placeholder: placeholder %> + + + <%= options_for_select(@censor_rule.canned_replacements) %> + +
- put it in [square brackets], e.g. [personal information removed]. applies to text in emails and HTML conversions of binaries; binaries themselves must stay the same length and the replacement is just a bunch of 'x's + Put it in [square brackets], e.g. + [Name removed]. Applies to text in emails and HTML conversions of + binaries; binaries themselves must stay the same length and the + replacement is just a bunch of 'x's
diff --git a/app/views/admin_censor_rule/_show.html.erb b/app/views/admin_censor_rule/_show.html.erb index 67b6f67e17..98ae703cdf 100644 --- a/app/views/admin_censor_rule/_show.html.erb +++ b/app/views/admin_censor_rule/_show.html.erb @@ -6,15 +6,22 @@ <% CensorRule.content_columns.each do |column| %> <%= column.name.humanize %> <% end %> + Applies to Actions <% censor_rules.each do |censor_rule| %> <%=h censor_rule.id %> + <% CensorRule.content_columns.map { |c| c.name }.each do |column| %> <%=h censor_rule.send(column) %> <% end %> + + + <%= censor_rule_applicable_class(censor_rule) %> + + <%= link_to "Edit", edit_admin_censor_rule_path(censor_rule) %> diff --git a/app/views/admin_comment/edit.html.erb b/app/views/admin_comment/edit.html.erb index 9cb1e4f3df..61ffe7e13b 100644 --- a/app/views/admin_comment/edit.html.erb +++ b/app/views/admin_comment/edit.html.erb @@ -2,6 +2,26 @@ <%= foi_error_messages_for 'comment' %> +
+
+ + + + + + + + + + +
+ By <%= both_links(@comment.user) %> +
+ On <%= both_links(@comment.info_request) %> +
+
+
+ <%= form_tag admin_comment_path(@comment), :method => 'put' do %>


@@ -22,11 +42,6 @@

<%= submit_tag 'Save', :accesskey => 's', :class => 'btn btn-success' %>

<% end %> -

- <%= link_to 'Show request', admin_request_path(@comment.info_request) %> | - <%= link_to 'List all requests', admin_requests_path %> -

-

Events

diff --git a/app/views/admin_comment/index.html.erb b/app/views/admin_comment/index.html.erb index 5ed06aa746..f422dd8d56 100644 --- a/app/views/admin_comment/index.html.erb +++ b/app/views/admin_comment/index.html.erb @@ -1,43 +1,15 @@

<%= @title %>

-<%= form_tag({}, :method => :get, :class => 'form form-search') do %> - <%= text_field_tag 'query', params[:query], { :size => 30, :class => 'input-large search-query' } %> - <%= submit_tag 'Search', :class => 'btn' %> (substring search, body) -<% end %> - -
-<% @comments.each do |comment| %> -
-
- - <%= chevron_right %> - - <%= comment_labels(comment) %> - - <%= link_to "#{ h(truncate(comment.body, :length => 100)) }", - edit_admin_comment_path(comment) %> - - -
- -
- - - <% comment.for_admin_column do |name, value, type|%> - - - - - <% end %> - -
<%= h name %> - <%= admin_value(value) %> -
-
+<%= form_tag({}, method: :get, class: 'form form-search') do %> +
+ <%= text_field_tag 'query', params[:query], size: 30, class: 'input-large search-query' %> + <%= submit_tag 'Search', class: 'btn' %>
+ + (substring search, body) <% end %> -
-<%= will_paginate(@comments, :class => "paginator") %> +<%= render partial: 'admin_request/some_annotations' , + locals: { comments: @comments } %> + +<%= will_paginate(@comments, class: 'paginator') %> diff --git a/app/views/admin_general/_admin_navbar.html.erb b/app/views/admin_general/_admin_navbar.html.erb index 4732713a87..4b9c0f39c0 100644 --- a/app/views/admin_general/_admin_navbar.html.erb +++ b/app/views/admin_general/_admin_navbar.html.erb @@ -1,10 +1,17 @@
-