From: Andy Allan Date: Wed, 27 Nov 2024 17:02:25 +0000 (+0000) Subject: Merge pull request #4316 from AntonKhorev/block-reason-max-length X-Git-Tag: live~83 X-Git-Url: https://git.openstreetmap.org./rails.git/commitdiff_plain/c47a40c1db59f0d0f836fbcd1b876390befe796e?hp=30caa3c09ee2108fa5ed77883e4d658d4b58d9af Merge pull request #4316 from AntonKhorev/block-reason-max-length Set max length of block reason --- diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..b5f38b76b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ + + +### Description + + +### How has this been tested? + diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml new file mode 100644 index 000000000..67a676d87 --- /dev/null +++ b/.github/workflows/danger.yml @@ -0,0 +1,35 @@ +name: Danger + +on: + pull_request_target: + types: [opened, synchronize] + +permissions: + pull-requests: write + statuses: write + +jobs: + danger: + runs-on: ubuntu-22.04 + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.1 + rubygems: 3.4.10 + bundler-cache: true + - name: Create base branch + run: | + git fetch ${{ github.event.pull_request.base.repo.clone_url }} ${{ github.event.pull_request.base.ref }}:danger_base + - name: Create head branch + run: | + git fetch ${{ github.event.pull_request.head.repo.clone_url }} ${{ github.event.pull_request.head.ref }}:danger_head + - name: Danger + env: + DANGER_GITHUB_BEARER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + bundle exec danger --verbose diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9fc132014..343084b0f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ concurrency: cancel-in-progress: true env: os: ubuntu-22.04 - ruby: '3.0' + ruby: '3.1' jobs: rubocop: name: RuboCop diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 46ab75482..25722c1f4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,8 +10,8 @@ jobs: name: Ubuntu ${{ matrix.ubuntu }}, Ruby ${{ matrix.ruby }} strategy: matrix: - ubuntu: [20.04, 22.04] - ruby: ['3.0', '3.1', '3.2', '3.3'] + ubuntu: [22.04, 24.04] + ruby: ['3.1', '3.2', '3.3'] runs-on: ubuntu-${{ matrix.ubuntu }} env: RAILS_ENV: test @@ -58,10 +58,14 @@ jobs: run: bundle exec bin/yarn install - name: Compile assets run: bundle exec rails assets:precompile + - name: Create tmp/pids directory + run: mkdir -p tmp/pids - name: Run tests run: bundle exec rails test:all + - name: Run javascript tests + run: bundle exec teaspoon - name: Report completion to Coveralls - uses: coverallsapp/github-action@v2.3.0 + uses: coverallsapp/github-action@v2.3.4 with: github-token: ${{ secrets.github_token }} flag-name: ubuntu-${{ matrix.ubuntu }}-ruby-${{ matrix.ruby }} @@ -73,7 +77,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Report completion to Coveralls - uses: coverallsapp/github-action@v2.3.0 + uses: coverallsapp/github-action@v2.3.4 with: github-token: ${{ secrets.github_token }} parallel-finished: true diff --git a/.rubocop.yml b/.rubocop.yml index c0f0c1fa0..8b6ed0180 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -9,7 +9,7 @@ require: - rubocop-rake AllCops: - TargetRubyVersion: 3.0 + TargetRubyVersion: 3.1 NewCops: enable Exclude: - 'vendor/**/*' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3b18f7246..7384a8d95 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2023-04-11 16:47:05 UTC using RuboCop version 1.50.0. +# on 2024-08-27 18:01:13 UTC using RuboCop version 1.65.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -14,19 +14,28 @@ require: - rubocop-rails - rubocop-rake -# Offense count: 11 -# Configuration parameters: Include, MaxAmount +# Offense count: 13 +# Configuration parameters: Include, MaxAmount. +# Include: **/*_spec.rb, **/spec/**/*, **/test/**/*, **/features/support/factories/**/*.rb FactoryBot/ExcessiveCreateList: - MaxAmount: 200 - -# Offense count: 557 + Exclude: + - 'test/controllers/api/changeset_comments_controller_test.rb' + - 'test/controllers/api/messages_controller_test.rb' + - 'test/controllers/changesets_controller_test.rb' + - 'test/controllers/diary_entries_controller_test.rb' + - 'test/controllers/notes_controller_test.rb' + - 'test/controllers/traces_controller_test.rb' + - 'test/controllers/user_blocks_controller_test.rb' + - 'test/system/users_test.rb' + +# Offense count: 635 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. # URISchemes: http, https Layout/LineLength: - Max: 266 + Max: 234 -# Offense count: 29 +# Offense count: 23 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: AllowSafeAssignment. Lint/AssignmentInCondition: @@ -34,9 +43,7 @@ Lint/AssignmentInCondition: - 'app/controllers/accounts_controller.rb' - 'app/controllers/api/traces_controller.rb' - 'app/controllers/api/user_preferences_controller.rb' - - 'app/controllers/application_controller.rb' - 'app/controllers/geocoder_controller.rb' - - 'app/controllers/notes_controller.rb' - 'app/controllers/traces_controller.rb' - 'app/controllers/users_controller.rb' - 'app/helpers/application_helper.rb' @@ -47,33 +54,33 @@ Lint/AssignmentInCondition: - 'lib/osm.rb' - 'script/deliver-message' -# Offense count: 680 +# Offense count: 762 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 189 -# Offense count: 73 +# Offense count: 37 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # AllowedMethods: refine Metrics/BlockLength: Max: 71 -# Offense count: 20 -# Configuration parameters: CountBlocks. +# Offense count: 8 +# Configuration parameters: CountBlocks, CountModifierForms. Metrics/BlockNesting: Max: 5 # Offense count: 26 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 314 + Max: 309 -# Offense count: 59 +# Offense count: 58 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: - Max: 31 + Max: 25 -# Offense count: 753 +# Offense count: 844 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Max: 179 @@ -83,35 +90,33 @@ Metrics/MethodLength: Metrics/ParameterLists: Max: 6 -# Offense count: 56 +# Offense count: 58 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Max: 32 + Max: 26 -# Offense count: 2394 +# Offense count: 2784 # This cop supports safe autocorrection (--autocorrect). Minitest/EmptyLineBeforeAssertionMethods: Enabled: false -# Offense count: 565 +# Offense count: 695 Minitest/MultipleAssertions: Max: 60 -# Offense count: 1 +# Offense count: 10 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/ActionControllerFlashBeforeRender: Exclude: + - 'app/controllers/application_controller.rb' + - 'app/controllers/confirmations_controller.rb' + - 'app/controllers/friendships_controller.rb' + - 'app/controllers/issue_comments_controller.rb' + - 'app/controllers/messages_controller.rb' + - 'app/controllers/passwords_controller.rb' - 'app/controllers/traces_controller.rb' - -# Offense count: 5 -# Configuration parameters: Database, Include. -# SupportedDatabases: mysql, postgresql -# Include: db/migrate/*.rb -Rails/BulkChangeTable: - Exclude: - - 'db/migrate/20111116184519_update_oauth.rb' - - 'db/migrate/20120208194454_add_domain_to_acl.rb' - - 'db/migrate/20120404205604_add_user_and_description_to_redaction.rb' + - 'app/controllers/user_blocks_controller.rb' + - 'app/controllers/users_controller.rb' # Offense count: 2 # Configuration parameters: Include. @@ -128,7 +133,7 @@ Rails/HelperInstanceVariable: Exclude: - 'app/helpers/title_helper.rb' -# Offense count: 16 +# Offense count: 17 # Configuration parameters: IgnoreScopes, Include. # Include: app/models/**/*.rb Rails/InverseOf: @@ -141,17 +146,17 @@ Rails/InverseOf: - 'app/models/note.rb' - 'app/models/user.rb' -# Offense count: 2 +# Offense count: 1 # Configuration parameters: Include. # Include: app/controllers/**/*.rb, app/mailers/**/*.rb Rails/LexicallyScopedActionFilter: Exclude: - 'app/controllers/oauth2_applications_controller.rb' - - 'app/controllers/oauth2_authorizations_controller.rb' # Offense count: 5 -# Configuration parameters: Include. -# Include: db/migrate/*.rb +# Configuration parameters: Database, Include. +# SupportedDatabases: mysql +# Include: db/**/*.rb Rails/NotNullColumn: Exclude: - 'db/migrate/002_cleanup_osm_db.rb' @@ -187,14 +192,14 @@ Rake/Desc: - 'lib/tasks/subscribe_diary_authors.rake' - 'lib/tasks/subscribe_old_changesets.rake' -# Offense count: 632 +# Offense count: 712 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never Style/FrozenStringLiteralComment: Enabled: false -# Offense count: 72 +# Offense count: 78 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Strict, AllowedNumbers, AllowedPatterns. Style/NumericLiterals: diff --git a/CONFIGURE.md b/CONFIGURE.md index dcc8ae2ac..29d1daad8 100644 --- a/CONFIGURE.md +++ b/CONFIGURE.md @@ -76,7 +76,7 @@ For iD, do the following: An example excerpt from settings.local.yml: -``` +```yaml # Default editor default_editor: "id" # OAuth 2 Client ID for iD @@ -99,7 +99,7 @@ To allow [Notes](https://wiki.openstreetmap.org/wiki/Notes) and changeset discus An example excerpt from settings.local.yml: -``` +```yaml # OAuth 2 Client ID for the web site oauth_application: "SGm8QJ6tmoPXEaUPIZzLUmm1iujltYZVWCp9hvGsqXg" # OAuth 2 Client Secret for the web site diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 383e793b3..e298c944f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,15 @@ +# Contributing + * https://www.ruby-lang.org/ - The homepage of Ruby which has more links and some great tutorials. * https://rubyonrails.org/ - The homepage of Rails, also has links and tutorials. +## Assigning Issues + +We don't assign issues to individual contributors. You are welcome to work on any +issue, and there's no need to ask first. + +For more details see [our FAQ](FAQ.md)] + ## Coding style We use [Rubocop](https://github.com/rubocop-hq/rubocop) (for ruby files) @@ -43,6 +52,12 @@ You can run the existing test suite with: bundle exec rails test:all ``` +You can run javascript tests with: + +``` +bundle exec teaspoon +``` + You can view test coverage statistics by browsing the `coverage` directory. The tests are automatically run on Pull Requests and other commits via github @@ -77,15 +92,6 @@ only submit changes to the `en.yml` file. The other files are updated via [Translatewiki](https://translatewiki.net/wiki/Translating:OpenStreetMap) and should not be included in your pull request. -### Nominatim prefixes - -I18n keys under the `geocoder.search_osm_nominatim` keyspace are managed by the -Nominatim maintainers. From time to time they run stats over the Nominatim -database, and update the list of available keys manually. - -Adding or removing keys to this list is therefore discouraged, but contributions -to the descriptive texts are welcome. - ### Copyright attribution The list of attributions on the /copyright page is managed by the [OSMF Licensing diff --git a/DOCKER.md b/DOCKER.md index f6caa6988..b93bf6d50 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -24,27 +24,37 @@ Use [Docker Engine](https://docs.docker.com/engine/install/ubuntu/) with the [do The first step is to fork/clone the repo to your local machine: - git clone https://github.com/openstreetmap/openstreetmap-website.git +``` +git clone https://github.com/openstreetmap/openstreetmap-website.git +``` Now change working directory to the `openstreetmap-website`: - cd openstreetmap-website +``` +cd openstreetmap-website +``` ## Initial Setup ### Storage - cp config/example.storage.yml config/storage.yml +``` +cp config/example.storage.yml config/storage.yml +``` ### Database - cp config/docker.database.yml config/database.yml +``` +cp config/docker.database.yml config/database.yml +``` ## Prepare local settings file This is a workaround. [See issues/2185 for details](https://github.com/openstreetmap/openstreetmap-website/issues/2185#issuecomment-508676026). - touch config/settings.local.yml +``` +touch config/settings.local.yml +``` **Windows users:** `touch` is not an availible command in Windows so just create a `settings.local.yml` file in the `config` directory, or if you have WSL you can run `wsl touch config/settings.local.yml`. @@ -52,13 +62,17 @@ This is a workaround. [See issues/2185 for details](https://github.com/openstree To build local Docker images run from the root directory of the repository: - docker compose build +``` +docker compose build +``` If this is your first time running or you have removed cache this will take some time to complete. Once the Docker images have finished building you can launch the images as containers. To launch the app run: - docker compose up -d +``` +docker compose up -d +``` This will launch one Docker container for each 'service' specified in `docker-compose.yml` and run them in the background. There are two options for inspecting the logs of these running containers: @@ -69,17 +83,29 @@ This will launch one Docker container for each 'service' specified in `docker-co Run the Rails database migrations: - docker compose run --rm web bundle exec rails db:migrate +``` +docker compose run --rm web bundle exec rails db:migrate +``` ### Tests Prepare the test database: - docker compose run --rm web bundle exec rails db:test:prepare +``` +docker compose run --rm web bundle exec rails db:test:prepare +``` Run the test suite: - docker compose run --rm web bundle exec rails test:all +``` +docker compose run --rm web bundle exec rails test:all +``` + +If you encounter errors about missing assets, precompile the assets: + +``` +docker compose run --rm web bundle exec rake assets:precompile +``` ### Loading an OSM extract @@ -87,31 +113,37 @@ This installation comes with no geographic data loaded. You can either create ne For example, let's download the District of Columbia from Geofabrik or [any other region](https://download.geofabrik.de): - wget https://download.geofabrik.de/north-america/us/district-of-columbia-latest.osm.pbf +``` +wget https://download.geofabrik.de/north-america/us/district-of-columbia-latest.osm.pbf +``` You can now use Docker to load this extract into your local Docker-based OSM instance: - docker compose run --rm web osmosis \ - -verbose \ - --read-pbf district-of-columbia-latest.osm.pbf \ - --log-progress \ - --write-apidb \ - host="db" \ - database="openstreetmap" \ - user="openstreetmap" \ - validateSchemaVersion="no" +``` +docker compose run --rm web osmosis \ + -verbose \ + --read-pbf district-of-columbia-latest.osm.pbf \ + --log-progress \ + --write-apidb \ + host="db" \ + database="openstreetmap" \ + user="openstreetmap" \ + validateSchemaVersion="no" +``` **Windows users:** Powershell uses `` ` `` and CMD uses `^` at the end of each line, e.g.: - docker compose run --rm web osmosis ` - -verbose ` - --read-pbf district-of-columbia-latest.osm.pbf ` - --log-progress ` - --write-apidb ` - host="db" ` - database="openstreetmap" ` - user="openstreetmap" ` - validateSchemaVersion="no" +``` +docker compose run --rm web osmosis ` + -verbose ` + --read-pbf district-of-columbia-latest.osm.pbf ` + --log-progress ` + --write-apidb ` + host="db" ` + database="openstreetmap" ` + user="openstreetmap" ` + validateSchemaVersion="no" +``` Once you have data loaded for Washington, DC you should be able to navigate to [`http://localhost:3000/#map=12/38.8938/-77.0146`](http://localhost:3000/#map=12/38.8938/-77.0146) to begin working with your local instance. @@ -123,12 +155,18 @@ See [`CONFIGURE.md`](CONFIGURE.md) for information on how to manage users and en If you want to get into a web container and run specific commands you can fire up a throwaway container to run bash in via: - docker compose run --rm web bash +``` +docker compose run --rm web bash +``` Alternatively, if you want to use the already-running `web` container then you can `exec` into it via: - docker compose exec web bash +``` +docker compose exec web bash +``` Similarly, if you want to `exec` in the db container use: - docker compose exec db bash +``` +docker compose exec db bash +``` diff --git a/Dangerfile b/Dangerfile new file mode 100644 index 000000000..6e2aeced8 --- /dev/null +++ b/Dangerfile @@ -0,0 +1,42 @@ +# Get PR number +pr_number = github.pr_json["number"] + +# Report if number of changed lines is > 500 +if git.lines_of_code > 500 + warn("Number of updated lines of code is too large to be in one PR. Perhaps it should be separated into two or more?") + auto_label.set(pr_number, "big-pr", "FBCA04") +else + auto_label.remove("big-pr") +end + +# Get list of translation files (except en.yml) which are modified +modified_yml_files = git.modified_files.select do |file| + file.start_with?("config/locales") && File.extname(file) == ".yml" && File.basename(file) != "en.yml" +end + +# Report if some translation file (except en.yml) is modified +if modified_yml_files.empty? + auto_label.remove("inappropriate-translations") +else + modified_files_str = modified_yml_files.map { |file| "`#{file}`" }.join(", ") + warn("The following YAML files other than `en.yml` have been modified: #{modified_files_str}. Only `en.yml` is allowed to be changed. Translations are updated via Translatewiki, see CONTRIBUTING.md.") + auto_label.set(pr_number, "inappropriate-translations", "B60205") +end + +# Report if there are merge-commits in PR +if git.commits.any? { |c| c.parents.count > 1 } + warn("Merge commits are found in PR. Please rebase to get rid of the merge commits in this PR, see CONTRIBUTING.md.") + auto_label.set(pr_number, "merge-commits", "D93F0B") +else + auto_label.remove("merge-commits") +end + +# Check if Gemfile is modified but Gemfile.lock is not +gemfile_modified = git.modified_files.include?("Gemfile") +gemfile_lock_modified = git.modified_files.include?("Gemfile.lock") +if gemfile_modified && !gemfile_lock_modified + warn("Gemfile was updated, but Gemfile.lock wasn't updated. Usually, when Gemfile is updated, you should run `bundle install` to update Gemfile.lock.") + auto_label.set(pr_number, "gemfile-lock-outdated", "F9D0C4") +else + auto_label.remove("gemfile-lock-outdated") +end diff --git a/Dockerfile b/Dockerfile index 1c5638275..dae25be3f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,51 +1,39 @@ -FROM ubuntu:22.04 +FROM debian:bookworm ENV DEBIAN_FRONTEND=noninteractive # Install system packages then clean up to minimize image size RUN apt-get update \ - && apt-get install --no-install-recommends -y \ - build-essential \ - curl \ - default-jre-headless \ - file \ - git-core \ - gpg-agent \ - libarchive-dev \ - libffi-dev \ - libgd-dev \ - libpq-dev \ - libsasl2-dev \ - libvips-dev \ - libxml2-dev \ - libxslt1-dev \ - libyaml-dev \ - locales \ - postgresql-client \ - ruby \ - ruby-dev \ - ruby-bundler \ - software-properties-common \ - tzdata \ - unzip - -# Install Node.js 18 and npm -RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ - && apt-get install -y nodejs + && apt-get install --no-install-recommends -y \ + build-essential \ + curl \ + default-jre-headless \ + file \ + git-core \ + gpg-agent \ + libarchive-dev \ + libffi-dev \ + libgd-dev \ + libpq-dev \ + libsasl2-dev \ + libvips-dev \ + libxml2-dev \ + libxslt1-dev \ + libyaml-dev \ + locales \ + postgresql-client \ + ruby-dev \ + ruby-bundler \ + tzdata \ + unzip \ + nodejs \ + npm \ + osmosis \ + ca-certificates \ + firefox-esr # Install yarn globally -RUN npm install --global yarn \ - # We can't use snap packages for firefox inside a container, so we need to get firefox+geckodriver elsewhere - && add-apt-repository -y ppa:mozillateam/ppa \ - && echo "Package: *\nPin: release o=LP-PPA-mozillateam\nPin-Priority: 1001" > /etc/apt/preferences.d/mozilla-firefox \ - && apt-get install --no-install-recommends -y \ - firefox-geckodriver \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install compatible Osmosis to help users import sample data in a new instance -RUN curl -OL https://github.com/openstreetmap/osmosis/releases/download/0.47.2/osmosis-0.47.2.tgz \ - && tar -C /usr/local -xzf osmosis-0.47.2.tgz +RUN npm install --global yarn ENV DEBIAN_FRONTEND=dialog diff --git a/FAQ.md b/FAQ.md index d4ac1fc9f..e53c8dddb 100644 --- a/FAQ.md +++ b/FAQ.md @@ -1,3 +1,5 @@ +# Frequently Asked Questions + ## How do I create a banner to promote my OpenStreetMap event? We occasionally display banner images on the main page of [openstreetmap.org](https://www.openstreetmap.org/) to @@ -23,3 +25,13 @@ drive. This is a great way to reach a lot of people! See [PR #1296](https://github.com/openstreetmap/openstreetmap-website/pull/1296) as an example. + +## Why don't you assign issues? + +We don't assign issues to volunteers for several reasons. The main reasons are that it discourages other volunteers from working on the issue, and the process turns into an unproductive administrative overhead for our team. + +There's no need to ask for an issue to be assigned before anyone starts working on it. Everyone is welcome to work on any issue at any time. + +In our experience, most people who ask for an issue to be assigned to them never create a pull request. So we would need to keep track of the assigned issues, and remember to unassign them a week or two into the future, when it is likely that they will not be making a PR. Assigned developers might feel bad if they perceive that we're unhappy with their progress, further discouraging them from contributing. Or we will get drawn into discussions about needing more time, or re-assigning them again, or so on. So it is best not to assign in the first place. + +The risk that two people are both genuinely working on the same task in the same hour or two is vanishingly remote, and doesn't outweigh the downsides described above. A better approach is to encourage people to simply work on the task and create a pull request, at which point everyone knows that they are actually working on the issue and not just planning/hoping/wishing to do so. diff --git a/Gemfile b/Gemfile index ac056cd83..277346b83 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" # Require rails -gem "rails", "~> 7.1.0" +gem "rails", "~> 7.2.0" gem "turbo-rails" # Require json for multi_json @@ -59,7 +59,6 @@ gem "dry-validation" gem "frozen_record" gem "http_accept_language", "~> 2.1.1" gem "i18n-js", "~> 3.9.2" -gem "oauth-plugin", ">= 0.5.1" gem "openstreetmap-deadlock_retry", ">= 1.3.1", :require => "deadlock_retry" gem "rack-cors" gem "rails-i18n", "~> 7.0.0" @@ -76,6 +75,7 @@ gem "addressable", "~> 2.8" gem "rack-uri_sanitizer" # Omniauth for authentication +gem "multi_json" gem "omniauth", "~> 2.0.2" gem "omniauth-facebook" gem "omniauth-github" @@ -130,7 +130,7 @@ gem "gd2-ffij", ">= 0.4.0" gem "marcel" # Used for browser detection -gem "browser", "< 6" # for ruby 3.0 support +gem "browser", "< 6" # for ruby 3.1 support # Used for S3 object storage gem "aws-sdk-s3" @@ -141,13 +141,15 @@ gem "image_processing" # Used to validate widths gem "unicode-display_width" -# Keep ruby 3.0 compatibility -gem "multi_xml", "~> 0.6.0" +# Lock some modules to old versions for ruby 3.1 support +gem "zeitwerk", "< 2.7" # Gems useful for development group :development do gem "better_errors" gem "binding_of_caller" + gem "danger" + gem "danger-auto_label" gem "debug_inspector" gem "i18n-tasks" gem "listen" @@ -181,6 +183,8 @@ end group :development, :test do gem "annotate" + gem "teaspoon" + gem "teaspoon-mocha", "~> 2.3.3" # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", :require => "debug/prelude" diff --git a/Gemfile.lock b/Gemfile.lock index e5ee05cea..2b23ab342 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,86 +3,83 @@ GEM specs: aasm (5.5.0) concurrent-ruby (~> 1.0) - actioncable (7.1.3.4) - actionpack (= 7.1.3.4) - activesupport (= 7.1.3.4) + actioncable (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.3.4) - actionpack (= 7.1.3.4) - activejob (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) - mail (>= 2.7.1) - net-imap - net-pop - net-smtp - actionmailer (7.1.3.4) - actionpack (= 7.1.3.4) - actionview (= 7.1.3.4) - activejob (= 7.1.3.4) - activesupport (= 7.1.3.4) - mail (~> 2.5, >= 2.5.4) - net-imap - net-pop - net-smtp + actionmailbox (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) + mail (>= 2.8.0) + actionmailer (7.2.2) + actionpack (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activesupport (= 7.2.2) + mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.1.3.4) - actionview (= 7.1.3.4) - activesupport (= 7.1.3.4) + actionpack (7.2.2) + actionview (= 7.2.2) + activesupport (= 7.2.2) nokogiri (>= 1.8.5) racc - rack (>= 2.2.4) + rack (>= 2.2.4, < 3.2) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) actionpack-page_caching (1.2.4) actionpack (>= 4.0.0) - actiontext (7.1.3.4) - actionpack (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + actiontext (7.2.2) + actionpack (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.3.4) - activesupport (= 7.1.3.4) + actionview (7.2.2) + activesupport (= 7.2.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) active_record_union (1.3.0) activerecord (>= 4.0) - activejob (7.1.3.4) - activesupport (= 7.1.3.4) + activejob (7.2.2) + activesupport (= 7.2.2) globalid (>= 0.3.6) - activemodel (7.1.3.4) - activesupport (= 7.1.3.4) - activerecord (7.1.3.4) - activemodel (= 7.1.3.4) - activesupport (= 7.1.3.4) + activemodel (7.2.2) + activesupport (= 7.2.2) + activerecord (7.2.2) + activemodel (= 7.2.2) + activesupport (= 7.2.2) timeout (>= 0.4.0) - activerecord-import (1.7.0) + activerecord-import (1.8.1) activerecord (>= 4.2) - activestorage (7.1.3.4) - actionpack (= 7.1.3.4) - activejob (= 7.1.3.4) - activerecord (= 7.1.3.4) - activesupport (= 7.1.3.4) + activestorage (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activesupport (= 7.2.2) marcel (~> 1.0) - activesupport (7.1.3.4) + activesupport (7.2.2) base64 + benchmark (>= 0.3) bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) annotate (3.2.0) @@ -95,22 +92,23 @@ GEM autoprefixer-rails (10.4.19.0) execjs (~> 2) aws-eventstream (1.3.0) - aws-partitions (1.968.0) - aws-sdk-core (3.201.5) + aws-partitions (1.1013.0) + aws-sdk-core (3.214.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) + aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.88.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.159.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-s3 (1.174.0) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.9.1) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) base64 (0.2.0) + benchmark (0.4.0) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -133,7 +131,7 @@ GEM bootstrap_form (5.4.0) actionpack (>= 6.1) activemodel (>= 6.1) - brakeman (6.2.1) + brakeman (6.2.2) racc brotli (0.6.0) browser (5.3.1) @@ -141,8 +139,8 @@ GEM bzip2-ffi (1.1.1) ffi (~> 1.0) cancancan (3.6.1) - canonical-rails (0.2.15) - actionview (>= 4.1, <= 7.2) + canonical-rails (0.2.16) + actionview (>= 4.1, < 7.3) capybara (3.40.0) addressable matrix @@ -154,15 +152,42 @@ GEM xpath (~> 3.2) childprocess (5.1.0) logger (~> 1.5) + claide (1.1.0) + claide-plugins (0.9.2) + cork + nap + open4 (~> 1.3) + colored2 (3.1.2) concurrent-ruby (1.3.4) - config (5.5.1) + config (5.5.2) deep_merge (~> 1.2, >= 1.2.1) + ostruct connection_pool (2.4.1) + cork (0.3.0) + colored2 (~> 3.1) crack (1.0.0) bigdecimal rexml crass (1.0.6) dalli (3.2.8) + danger (9.5.1) + base64 (~> 0.2) + claide (~> 1.0) + claide-plugins (>= 0.9.2) + colored2 (~> 3.1) + cork (~> 0.1) + faraday (>= 0.9.0, < 3.0) + faraday-http-cache (~> 2.0) + git (~> 1.13) + kramdown (~> 2.3) + kramdown-parser-gfm (~> 1.0) + octokit (>= 4.0) + pstore (~> 0.1) + terminal-table (>= 1, < 4) + danger-auto_label (1.3.1) + danger-plugin-api (~> 1.0) + danger-plugin-api (1.0.0) + danger (> 2.0) dartsass-ruby (3.0.2) sass-embedded (~> 1.54, < 1.67) dartsass-sprockets (3.0.0) @@ -171,16 +196,16 @@ GEM sprockets (> 3.0) sprockets-rails tilt - date (3.3.4) + date (3.4.0) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) debug_inspector (1.2.0) deep_merge (1.2.2) - delayed_job (4.1.12) - activesupport (>= 3.0, < 8.0) - delayed_job_active_record (4.1.10) - activerecord (>= 3.0, < 8.0) + delayed_job (4.1.13) + activesupport (>= 3.0, < 9.0) + delayed_job_active_record (4.1.11) + activerecord (>= 3.0, < 9.0) delayed_job (>= 3.0, < 5) docile (1.4.1) doorkeeper (5.7.1) @@ -194,8 +219,9 @@ GEM dry-configurable (1.2.0) dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) - dry-core (1.0.1) + dry-core (1.0.2) concurrent-ruby (~> 1.0) + logger zeitwerk (~> 2.6) dry-inflector (1.1.0) dry-initializer (3.1.1) @@ -224,7 +250,7 @@ GEM dry-initializer (~> 3.0) dry-schema (>= 1.12, < 2) zeitwerk (~> 2.6) - erb_lint (0.6.0) + erb_lint (0.7.0) activesupport better_html (>= 2.0.1) parser (>= 2.7.1.4) @@ -232,18 +258,21 @@ GEM rubocop (>= 1) smart_properties erubi (1.13.0) - execjs (2.9.1) + execjs (2.10.0) exifr (1.4.0) - factory_bot (6.4.6) + factory_bot (6.5.0) activesupport (>= 5.0.0) - factory_bot_rails (6.4.3) - factory_bot (~> 6.4) + factory_bot_rails (6.4.4) + factory_bot (~> 6.5) railties (>= 5.0.0) - faraday (2.10.1) - faraday-net_http (>= 2.0, < 3.2) + faraday (2.12.1) + faraday-net_http (>= 2.0, < 3.5) + json logger - faraday-net_http (3.1.1) - net-http + faraday-http-cache (2.5.1) + faraday (>= 0.8) + faraday-net_http (3.4.0) + net-http (>= 0.5.0) ffi (1.17.0) ffi-compiler (1.3.2) ffi (>= 1.15.5) @@ -251,21 +280,24 @@ GEM ffi-libarchive (1.1.14) ffi (~> 1.0) file_exists (0.2.0) - frozen_record (0.27.2) + frozen_record (0.27.4) activemodel fspath (3.1.2) gd2-ffij (0.4.0) ffi (>= 1.0.0) + git (1.19.1) + addressable (~> 2.8) + rchardet (~> 1.8) globalid (1.2.1) activesupport (>= 6.1) - google-protobuf (3.25.4) - hashdiff (1.1.1) + google-protobuf (3.25.5) + hashdiff (1.1.2) hashie (5.0.0) - highline (3.1.0) + highline (3.1.1) reline htmlentities (4.3.4) http_accept_language (2.1.1) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) i18n-js (3.9.2) i18n (>= 0.6.6) @@ -279,7 +311,7 @@ GEM rails-i18n rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) - image_optim (0.31.3) + image_optim (0.31.4) exifr (~> 1.2, >= 1.2.2) fspath (~> 3.0) image_size (>= 1.5, < 4) @@ -296,10 +328,10 @@ GEM in_threads (1.6.0) iniparse (1.5.0) io-console (0.7.2) - irb (1.14.0) + irb (1.14.1) rdoc (>= 4.0.0) reline (>= 0.4.2) - jbuilder (2.12.0) + jbuilder (2.13.0) actionview (>= 5.0.0) activesupport (>= 5.0.0) jmespath (1.6.2) @@ -307,23 +339,25 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.7.2) - jwt (2.8.2) + json (2.8.2) + jwt (2.9.3) base64 kgio (2.11.4) - kramdown (2.4.0) - rexml + kramdown (2.5.1) + rexml (>= 3.3.9) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) language_server-protocol (3.17.0.3) libv8-node (18.19.0.0) libxml-ruby (5.0.3) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - logger (1.6.0) + logger (1.6.1) logstasher (2.1.5) activesupport (>= 5.2) request_store - loofah (2.22.0) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -336,19 +370,20 @@ GEM maxminddb (0.1.22) mini_magick (4.13.2) mini_mime (1.1.5) - mini_portile2 (2.8.7) + mini_portile2 (2.8.8) mini_racer (0.9.0) libv8-node (~> 18.19.0.0) - minitest (5.25.1) + minitest (5.25.2) minitest-focus (1.4.0) minitest (>= 4, < 6) - msgpack (1.7.2) + msgpack (1.7.5) multi_json (1.15.0) - multi_xml (0.6.0) - mutex_m (0.2.0) - net-http (0.4.1) + multi_xml (0.7.1) + bigdecimal (~> 3.1) + nap (1.1.0) + net-http (0.5.0) uri - net-imap (0.4.14) + net-imap (0.5.1) date net-protocol net-pop (0.1.2) @@ -357,16 +392,16 @@ GEM timeout net-smtp (0.5.0) net-protocol - nio4r (2.7.3) + nio4r (2.7.4) nokogiri (1.16.7) mini_portile2 (~> 2.8.2) racc (~> 1.4) - oauth (0.4.7) - oauth-plugin (0.5.1) - multi_json - oauth (~> 0.4.4) - oauth2 (>= 0.5.0) - rack + oauth (1.1.0) + oauth-tty (~> 1.0, >= 1.0.1) + snaky_hash (~> 2.0) + version_gem (~> 1.1) + oauth-tty (1.0.5) + version_gem (~> 1.1, >= 1.1.1) oauth2 (2.0.9) faraday (>= 0.17.3, < 3.0) jwt (>= 1.0, < 3.0) @@ -374,6 +409,9 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0) version_gem (~> 1.1) + octokit (9.2.0) + faraday (>= 1, < 3) + sawyer (~> 0.9) omniauth (2.0.4) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -384,8 +422,8 @@ GEM omniauth-github (2.0.1) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) - omniauth-google-oauth2 (1.1.2) - jwt (>= 2.0) + omniauth-google-oauth2 (1.2.0) + jwt (>= 2.9) oauth2 (~> 2.0) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8) @@ -396,9 +434,10 @@ GEM jwt (~> 2.0) omniauth (~> 2.0) omniauth-oauth2 (~> 1.8.0) - omniauth-oauth (1.2.0) + omniauth-oauth (1.2.1) oauth omniauth (>= 1.0, < 3) + rack (>= 1.6.2, < 4) omniauth-oauth2 (1.8.0) oauth2 (>= 1.4, < 3) omniauth (~> 2.0) @@ -408,26 +447,29 @@ GEM omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) + open4 (1.3.4) openstreetmap-deadlock_retry (1.3.1) - overcommit (0.64.0) + ostruct (0.6.1) + overcommit (0.64.1) childprocess (>= 0.6.3, < 6) iniparse (~> 1.4) - rexml (~> 3.2) + rexml (>= 3.3.9) parallel (1.26.3) - parser (3.3.4.2) + parser (3.3.6.0) ast (~> 2.4.1) racc - pg (1.5.7) + pg (1.5.9) popper_js (2.11.8) progress (3.6.0) - psych (5.1.2) + pstore (0.1.3) + psych (5.2.0) stringio public_suffix (6.0.1) - puma (5.6.8) + puma (5.6.9) nio4r (~> 2.0) quad_tile (1.0.1) racc (1.8.1) - rack (2.2.9) + rack (2.2.10) rack-cors (2.0.2) rack (>= 2.0.0) rack-openid (1.4.2) @@ -441,23 +483,23 @@ GEM rack-test (2.1.0) rack (>= 1.3) rack-uri_sanitizer (0.0.2) - rackup (1.0.0) + rackup (1.0.1) rack (< 3) webrick - rails (7.1.3.4) - actioncable (= 7.1.3.4) - actionmailbox (= 7.1.3.4) - actionmailer (= 7.1.3.4) - actionpack (= 7.1.3.4) - actiontext (= 7.1.3.4) - actionview (= 7.1.3.4) - activejob (= 7.1.3.4) - activemodel (= 7.1.3.4) - activerecord (= 7.1.3.4) - activestorage (= 7.1.3.4) - activesupport (= 7.1.3.4) + rails (7.2.2) + actioncable (= 7.2.2) + actionmailbox (= 7.2.2) + actionmailer (= 7.2.2) + actionpack (= 7.2.2) + actiontext (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activemodel (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) bundler (>= 1.15.0) - railties (= 7.1.3.4) + railties (= 7.2.2) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -469,16 +511,16 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - rails-i18n (7.0.9) + rails-i18n (7.0.10) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) rails_param (1.3.1) actionpack (>= 3.2.0) activesupport (>= 3.2.0) - railties (7.1.3.4) - actionpack (= 7.1.3.4) - activesupport (= 7.1.3.4) - irb + railties (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) + irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) @@ -488,47 +530,46 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rdoc (6.7.0) + rchardet (1.8.0) + rdoc (6.8.1) psych (>= 4.0.0) regexp_parser (2.9.2) - reline (0.5.9) + reline (0.5.11) io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) - rexml (3.3.6) - strscan + rexml (3.3.9) rinku (2.0.6) rotp (6.3.0) - rouge (4.3.0) + rouge (4.5.1) rtlcss (0.2.1) mini_racer (>= 0.6.3) - rubocop (1.65.1) + rubocop (1.69.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.4, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.31.1, < 2.0) + rubocop-ast (>= 1.36.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.32.1) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.36.1) parser (>= 3.3.1.0) rubocop-capybara (2.21.0) rubocop (~> 1.41) rubocop-factory_bot (2.26.1) rubocop (~> 1.61) - rubocop-minitest (0.35.1) + rubocop-minitest (0.36.0) rubocop (>= 1.61, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-performance (1.21.1) + rubocop-performance (1.23.0) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.25.1) + rubocop-rails (2.27.0) activesupport (>= 4.2.0) rack (>= 1.1) - rubocop (>= 1.33.0, < 2.0) + rubocop (>= 1.52.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) rubocop-rake (0.6.0) rubocop (~> 1.0) @@ -544,6 +585,10 @@ GEM sass-embedded (1.64.2) google-protobuf (~> 3.23) rake (>= 13.0.0) + sawyer (0.9.2) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + securerandom (0.3.2) selenium-webdriver (4.23.0) base64 (~> 0.2) logger (~> 1.4) @@ -554,7 +599,7 @@ GEM docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) - simplecov-html (0.12.3) + simplecov-html (0.13.1) simplecov-lcov (0.8.0) simplecov_json_formatter (0.1.4) simpleidn (0.2.3) @@ -575,39 +620,42 @@ GEM stringio (3.1.1) strong_migrations (1.8.0) activerecord (>= 5.2) - strscan (3.1.0) + teaspoon (1.4.0) + railties (>= 5.0) + teaspoon-mocha (2.3.3) + teaspoon (>= 1.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - terser (1.2.3) + terser (1.2.4) execjs (>= 0.3.0, < 3) - thor (1.3.1) + thor (1.3.2) tilt (2.4.0) - timeout (0.4.1) - turbo-rails (2.0.6) + timeout (0.4.2) + turbo-rails (2.0.11) actionpack (>= 6.0.0) - activejob (>= 6.0.0) railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - unicode-display_width (2.5.0) - uri (0.13.0) + unicode-display_width (2.6.0) + uri (0.13.1) + useragent (0.16.10) validates_email_format_of (1.8.2) i18n (>= 0.8.0) simpleidn vendorer (0.2.0) version_gem (1.1.4) - webmock (3.23.1) + webmock (3.24.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.8.1) + webrick (1.9.0) websocket (1.2.11) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.17) + zeitwerk (2.6.18) PLATFORMS ruby @@ -636,6 +684,8 @@ DEPENDENCIES config connection_pool dalli + danger + danger-auto_label dartsass-sprockets debug debug_inspector @@ -671,8 +721,7 @@ DEPENDENCIES mini_racer (~> 0.9.0) minitest (~> 5.1) minitest-focus - multi_xml (~> 0.6.0) - oauth-plugin (>= 0.5.1) + multi_json omniauth (~> 2.0.2) omniauth-facebook omniauth-github @@ -688,7 +737,7 @@ DEPENDENCIES quad_tile (~> 1.0.1) rack-cors rack-uri_sanitizer - rails (~> 7.1.0) + rails (~> 7.2.0) rails-controller-testing rails-i18n (~> 7.0.0) rails_param @@ -709,12 +758,15 @@ DEPENDENCIES simplecov-lcov sprockets-exporters_pack strong_migrations (< 2.0.0) + teaspoon + teaspoon-mocha (~> 2.3.3) terser turbo-rails unicode-display_width validates_email_format_of (>= 1.5.1) vendorer webmock + zeitwerk (< 2.7) BUNDLED WITH - 2.4.19 + 2.5.22 diff --git a/INSTALL.md b/INSTALL.md index c8e28a62f..8667fb512 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -9,7 +9,7 @@ are two alternatives which make it easier to get a consistent development enviro * **Vagrant** This installs the software into a virtual machine. For Vagrant instructions see [VAGRANT.md](VAGRANT.md). * **Docker** This installs the software using containerization. For Docker instructions see [DOCKER.md](DOCKER.md). -These instructions are based on Ubuntu 22.04 LTS, which is the platform used by the OSMF servers. +These instructions are based on Ubuntu 24.04 LTS, which is the platform used by the OSMF servers. The instructions also work, with only minor amendments, for all other current Ubuntu releases, Fedora and MacOSX We don't recommend attempting to develop or deploy this software on Windows. Some Ruby gems may not be supported. If you need to use Windows the easiest solutions in order are [Docker](DOCKER.md), [Vagrant](VAGRANT.md), and Ubuntu in a virtual machine. @@ -22,12 +22,12 @@ of packages required before you can get the various gems installed. ## Minimum requirements -* Ruby 3.0+ +* Ruby 3.1+ * PostgreSQL 13+ * Bundler (see note below about [developer Ruby setup](#rbenv)) * Javascript Runtime -These can be installed on Ubuntu 22.04 or later with: +These can be installed on Ubuntu 24.04 or later with: ``` sudo apt-get update diff --git a/Vagrantfile b/Vagrantfile index 7895d3b86..617bd7b4d 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -2,9 +2,11 @@ # vi: set ft=ruby : Vagrant.configure("2") do |config| - # use official ubuntu image for virtualbox + # use official debian image + config.vm.box = "debian/bookworm64" + + # configure virtualbox provider config.vm.provider "virtualbox" do |vb, override| - override.vm.box = "ubuntu/jammy64" override.vm.synced_folder ".", "/srv/openstreetmap-website" vb.customize ["modifyvm", :id, "--memory", "4096"] vb.customize ["modifyvm", :id, "--cpus", "2"] @@ -14,16 +16,16 @@ Vagrant.configure("2") do |config| # Use sshfs sharing if available, otherwise NFS sharing sharing_type = Vagrant.has_plugin?("vagrant-sshfs") ? "sshfs" : "nfs" - # use third party image and sshfs or NFS sharing for lxc + # configure lxc provider config.vm.provider "lxc" do |_, override| - override.vm.box = "generic/ubuntu2204" override.vm.synced_folder ".", "/srv/openstreetmap-website", :type => sharing_type end - # use third party image and sshfs or NFS sharing for libvirt - config.vm.provider "libvirt" do |_, override| - override.vm.box = "generic/ubuntu2204" + # configure libvirt provider + config.vm.provider "libvirt" do |libvirt, override| override.vm.synced_folder ".", "/srv/openstreetmap-website", :type => sharing_type + libvirt.memory = 4096 + libvirt.cpus = 2 end # configure shared package cache if possible diff --git a/app/abilities/ability.rb b/app/abilities/ability.rb index 7ee75f3bc..725ceccb2 100644 --- a/app/abilities/ability.rb +++ b/app/abilities/ability.rb @@ -7,51 +7,48 @@ class Ability can :query, :browse can :show, [Node, Way, Relation] can [:index, :show], [OldNode, OldWay, OldRelation] - can [:show, :new], Note + can [:show, :create], Note can :search, :direction can [:index, :permalink, :edit, :help, :fixthemap, :offline, :export, :about, :communities, :preview, :copyright, :key, :id], :site can [:finish, :embed], :export can [:search, :search_latlon, :search_osm_nominatim, :search_osm_nominatim_reverse], :geocoder - can [:token, :request_token, :access_token, :test_request], :oauth if Settings.status != "database_offline" can [:index, :feed, :show], Changeset - can :index, ChangesetComment + can :show, ChangesetComment can [:confirm, :confirm_resend, :confirm_email], :confirmation can [:index, :rss, :show], DiaryEntry can :index, DiaryComment can [:index], Note - can [:new, :create, :edit, :update], :password + can [:create, :update], :password can [:index, :show], Redaction - can [:new, :create, :destroy], :session + can [:create, :destroy], :session can [:index, :show, :data, :georss], Trace - can [:terms, :new, :create, :save, :suspended, :show, :auth_success, :auth_failure], User + can [:terms, :create, :save, :suspended, :show, :auth_success, :auth_failure], User can [:index, :show, :blocks_on, :blocks_by], UserBlock end if user&.active? can :welcome, :site - can [:revoke, :authorize], :oauth can [:show], :deletion if Settings.status != "database_offline" can [:subscribe, :unsubscribe], Changeset - can [:index, :new, :create, :show, :edit, :update, :destroy], ClientApplication - can [:index, :new, :create, :show, :edit, :update, :destroy], :oauth2_application + can [:index, :create, :show, :update, :destroy], :oauth2_application can [:index, :destroy], :oauth2_authorized_application - can [:new, :show, :create, :destroy], :oauth2_authorization - can [:edit, :update, :destroy], :account + can [:show, :create, :destroy], :oauth2_authorization + can [:update, :destroy], :account can [:show], :dashboard - can [:new, :create, :subscribe, :unsubscribe], DiaryEntry + can [:create, :subscribe, :unsubscribe], DiaryEntry can :update, DiaryEntry, :user => user can [:create], DiaryComment can [:make_friend, :remove_friend], Friendship - can [:new, :create, :reply, :show, :inbox, :outbox, :muted, :mark, :unmute, :destroy], Message + can [:create, :reply, :show, :inbox, :outbox, :muted, :mark, :unmute, :destroy], Message can [:close, :reopen], Note - can [:show, :edit, :update], :preference - can [:edit, :update], :profile - can [:new, :create], Report - can [:mine, :new, :create, :edit, :update, :destroy], Trace + can [:show, :update], :preference + can :update, :profile + can :create, Report + can [:mine, :create, :update, :destroy], Trace can [:account, :go_public], User can [:index, :create, :destroy], UserMute @@ -59,8 +56,8 @@ class Ability can [:hide, :unhide], [DiaryEntry, DiaryComment] can [:index, :show, :resolve, :ignore, :reopen], Issue can :create, IssueComment - can [:new, :create, :edit, :update, :destroy], Redaction - can [:new, :create, :revoke_all], UserBlock + can [:create, :update, :destroy], Redaction + can [:create, :revoke_all], UserBlock can :update, UserBlock, :creator => user can :update, UserBlock, :revoker => user can :update, UserBlock, :active? => true @@ -71,7 +68,7 @@ class Ability can [:index, :show, :resolve, :ignore, :reopen], Issue can :create, IssueComment can [:set_status, :destroy, :index], User - can [:grant, :revoke], UserRole + can [:create, :destroy], UserRole end end end diff --git a/app/abilities/api_capability.rb b/app/abilities/api_capability.rb index 44e676345..0e953d50b 100644 --- a/app/abilities/api_capability.rb +++ b/app/abilities/api_capability.rb @@ -5,14 +5,11 @@ class ApiCapability def initialize(token) if Settings.status != "database_offline" - user = if token.respond_to?(:resource_owner_id) - User.find(token.resource_owner_id) - elsif token.respond_to?(:user) - token.user - end + user = User.find(token.resource_owner_id) if user&.active? can [:create, :comment, :close, :reopen], Note if scope?(token, :write_notes) + can [:create, :destroy], NoteSubscription if scope?(token, :write_notes) can [:show, :data], Trace if scope?(token, :read_gpx) can [:create, :update, :destroy], Trace if scope?(token, :write_gpx) can [:details], User if scope?(token, :read_prefs) @@ -31,7 +28,7 @@ class ApiCapability if user.moderator? can [:destroy, :restore], ChangesetComment if scope?(token, :write_api) can :destroy, Note if scope?(token, :write_notes) - can :redact, [OldNode, OldWay, OldRelation] if user&.terms_agreed? && (scope?(token, :write_api) || scope?(token, :write_redactions)) + can :redact, [OldNode, OldWay, OldRelation] if user&.terms_agreed? && scope?(token, :write_redactions) end end end diff --git a/app/assets/images/browse/node.svg b/app/assets/images/browse/node.svg new file mode 100644 index 000000000..35e592763 --- /dev/null +++ b/app/assets/images/browse/node.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/browse/relation.svg b/app/assets/images/browse/relation.svg new file mode 100644 index 000000000..9cb52fc67 --- /dev/null +++ b/app/assets/images/browse/relation.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/assets/images/browse/way.svg b/app/assets/images/browse/way.svg new file mode 100644 index 000000000..98eae574f --- /dev/null +++ b/app/assets/images/browse/way.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/assets/images/key/opnvkarte/bus_stop13.svg b/app/assets/images/key/opnvkarte/bus_stop13.svg deleted file mode 100644 index ae4ffeeaf..000000000 --- a/app/assets/images/key/opnvkarte/bus_stop13.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/assets/images/key/opnvkarte/bus_stop15.svg b/app/assets/images/key/opnvkarte/bus_stop15.svg deleted file mode 100644 index a25991954..000000000 --- a/app/assets/images/key/opnvkarte/bus_stop15.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/assets/images/key/opnvkarte/rail17.svg b/app/assets/images/key/opnvkarte/rail17.svg deleted file mode 100644 index 8cada29c9..000000000 --- a/app/assets/images/key/opnvkarte/rail17.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/app/assets/images/key/opnvkarte/stop13.svg b/app/assets/images/key/opnvkarte/stop13.svg deleted file mode 100644 index cfe65b344..000000000 --- a/app/assets/images/key/opnvkarte/stop13.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/assets/images/key/opnvkarte/stop15.svg b/app/assets/images/key/opnvkarte/stop15.svg deleted file mode 100644 index 3ae62672e..000000000 --- a/app/assets/images/key/opnvkarte/stop15.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 054742126..ead01a8ff 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,6 +1,5 @@ //= require jquery3 //= require jquery_ujs -//= require jquery.timers //= require jquery.throttle-debounce //= require js-cookie/dist/js.cookie //= require popper diff --git a/app/assets/javascripts/embed.js.erb b/app/assets/javascripts/embed.js.erb index 9a0ec07d3..66cd02b98 100644 --- a/app/assets/javascripts/embed.js.erb +++ b/app/assets/javascripts/embed.js.erb @@ -37,18 +37,16 @@ window.onload = function () { map.attributionControl.setPrefix(''); map.removeControl(map.attributionControl); - if (!args.layer || args.layer === "mapnik" || args.layer === "osmarender" || args.layer === "mapquest") { - new L.OSM.Mapnik(mapnikOptions).addTo(map); - } else if (args.layer === "cyclosm") { + if (args.layer === "cyclosm") { new L.OSM.CyclOSM().addTo(map); } else if (args.layer === "cyclemap" || args.layer === "cycle map") { new L.OSM.CycleMap(thunderforestOptions).addTo(map); } else if (args.layer === "transportmap") { new L.OSM.TransportMap(thunderforestOptions).addTo(map); - } else if (args.layer === "opnvkarte") { - new L.OSM.OPNVKarte().addTo(map); } else if (args.layer === "hot") { new L.OSM.HOT().addTo(map); + } else { + new L.OSM.Mapnik(mapnikOptions).addTo(map); } if (args.marker) { diff --git a/app/assets/javascripts/index.js b/app/assets/javascripts/index.js index 5c7b2d26e..c419f9321 100644 --- a/app/assets/javascripts/index.js +++ b/app/assets/javascripts/index.js @@ -1,7 +1,7 @@ //= require_self //= require leaflet.sidebar //= require leaflet.sidebar-pane -//= require leaflet.locatecontrol/src/L.Control.Locate +//= require leaflet.locatecontrol/dist/L.Control.Locate.umd //= require leaflet.locate //= require leaflet.layers //= require leaflet.key @@ -25,8 +25,6 @@ //= require qs/dist/qs $(document).ready(function () { - var loaderTimeout; - var map = new L.OSM.Map("map", { zoomControl: false, layerControl: false, @@ -39,11 +37,7 @@ $(document).ready(function () { map.setSidebarOverlaid(false); - clearTimeout(loaderTimeout); - - loaderTimeout = setTimeout(function () { - $("#sidebar_loader").show(); - }, 200); + $("#sidebar_loader").show().addClass("delayed-fade-in"); // IE<10 doesn't respect Vary: X-Requested-With header, so // prevent caching the XHR response as a full-page URL. @@ -60,9 +54,8 @@ $(document).ready(function () { url: content_path, dataType: "html", complete: function (xhr) { - clearTimeout(loaderTimeout); $("#flash").empty(); - $("#sidebar_loader").hide(); + $("#sidebar_loader").removeClass("delayed-fade-in").hide(); var content = $(xhr.responseText); @@ -396,7 +389,7 @@ $(document).ready(function () { OSM.router.load(); $(document).on("click", "a", function (e) { - if (e.isDefaultPrevented() || e.isPropagationStopped()) { + if (e.isDefaultPrevented() || e.isPropagationStopped() || $(e.target).data("turbo")) { return; } @@ -412,6 +405,9 @@ $(document).ready(function () { if (OSM.router.route(this.pathname + this.search + this.hash)) { e.preventDefault(); + if (this.pathname !== "/directions") { + $("header").addClass("closed"); + } } }); diff --git a/app/assets/javascripts/index/new_note.js b/app/assets/javascripts/index/new_note.js index 59fbeeb1d..887ba043b 100644 --- a/app/assets/javascripts/index/new_note.js +++ b/app/assets/javascripts/index/new_note.js @@ -139,8 +139,6 @@ OSM.NewNote = function (map) { newNote.on("remove", function () { addNoteButton.removeClass("active"); - }).on("dragstart", function () { - $(newNote).stopTime("removenote"); }).on("dragend", function () { content.find("textarea").focus(); }); diff --git a/app/assets/javascripts/index/note.js b/app/assets/javascripts/index/note.js index 3839d2715..4e8f64d09 100644 --- a/app/assets/javascripts/index/note.js +++ b/app/assets/javascripts/index/note.js @@ -22,44 +22,50 @@ OSM.Note = function (map) { page.pushstate = page.popstate = function (path, id) { OSM.loadSidebarContent(path, function () { - initialize(path, id, function () { - var data = $(".details").data(); - if (!data) return; - var latLng = L.latLng(data.coordinates.split(",")); - if (!map.getBounds().contains(latLng)) moveToNote(); - }); + initialize(path, id); + + var data = $(".details").data(); + if (!data) return; + var latLng = L.latLng(data.coordinates.split(",")); + if (!map.getBounds().contains(latLng)) moveToNote(); }); }; page.load = function (path, id) { - initialize(path, id, moveToNote); + initialize(path, id); + moveToNote(); }; - function initialize(path, id, callback) { - content.find("button[type=submit]").on("click", function (e) { + function initialize(path, id) { + content.find("button[name]").on("click", function (e) { e.preventDefault(); var data = $(e.target).data(); - var form = e.target.form; - - $(form).find("button[type=submit]").prop("disabled", true); - - $.ajax({ + var name = $(e.target).attr("name"); + var ajaxSettings = { url: data.url, type: data.method, oauth: true, - data: { text: $(form.text).val() }, - success: function () { - OSM.loadSidebarContent(path, function () { - initialize(path, id, moveToNote); + success: () => { + OSM.loadSidebarContent(path, () => { + initialize(path, id); + moveToNote(); }); }, - error: function (xhr) { - $(form).find("#comment-error") + error: (xhr) => { + content.find("#comment-error") .text(xhr.responseText) - .prop("hidden", false); - updateButtons(form); + .prop("hidden", false) + .get(0).scrollIntoView({ block: "nearest" }); + updateButtons(); } - }); + }; + + if (name !== "subscribe" && name !== "unsubscribe") { + ajaxSettings.data = { text: $("textarea").val() }; + } + + content.find("button[name]").prop("disabled", true); + $.ajax(ajaxSettings); }); content.find("textarea").on("input", function (e) { @@ -78,18 +84,18 @@ OSM.Note = function (map) { icon: noteIcons[data.status] }); } - - if (callback) callback(); } - function updateButtons(form) { - $(form).find("button[type=submit]").prop("disabled", false); - if ($(form.text).val() === "") { - $(form.close).text($(form.close).data("defaultActionText")); - $(form.comment).prop("disabled", true); + function updateButtons() { + var resolveButton = content.find("button[name='close']"); + var commentButton = content.find("button[name='comment']"); + + content.find("button[name]").prop("disabled", false); + if (content.find("textarea").val() === "") { + resolveButton.text(resolveButton.data("defaultActionText")); + commentButton.prop("disabled", true); } else { - $(form.close).text($(form.close).data("commentActionText")); - $(form.comment).prop("disabled", false); + resolveButton.text(resolveButton.data("commentActionText")); } } diff --git a/app/assets/javascripts/index/query.js b/app/assets/javascripts/index/query.js index a7d228ff9..09e4de31e 100644 --- a/app/assets/javascripts/index/query.js +++ b/app/assets/javascripts/index/query.js @@ -253,9 +253,9 @@ OSM.Query = function (map) { * To find nearby objects we ask overpass for the union of the * following sets: * - * node(around:,,lng>) - * way(around:,,lng>) - * relation(around:,,lng>) + * node(around:,,) + * way(around:,,) + * relation(around:,,) * * to find enclosing objects we first find all the enclosing areas: * @@ -289,18 +289,10 @@ OSM.Query = function (map) { .hide(); if (marker) map.removeLayer(marker); - marker = L.circle(latlng, radius, featureStyle).addTo(map); - - $(document).everyTime(75, "fadeQueryMarker", function (i) { - if (i === 10) { - map.removeLayer(marker); - } else { - marker.setStyle({ - opacity: 1 - (i * 0.1), - fillOpacity: 0.5 - (i * 0.05) - }); - } - }, 10); + marker = L.circle(latlng, Object.assign({ + radius: radius, + className: "query-marker" + }, featureStyle)).addTo(map); runQuery(latlng, radius, nearby, $("#query-nearby"), false); runQuery(latlng, radius, isin, $("#query-isin"), true, compareSize); diff --git a/app/assets/javascripts/index/search.js b/app/assets/javascripts/index/search.js index 476ad30a1..2bfbb2e1c 100644 --- a/app/assets/javascripts/index/search.js +++ b/app/assets/javascripts/index/search.js @@ -32,6 +32,7 @@ OSM.Search = function (map) { $(".describe_location").on("click", function (e) { e.preventDefault(); + $("header").addClass("closed"); var center = map.getCenter().wrap(), precision = OSM.zoomPrecision(map.getZoom()), lat = center.lat.toFixed(precision), diff --git a/app/assets/javascripts/leaflet.layers.js b/app/assets/javascripts/leaflet.layers.js index 82efab506..dc692a4a5 100644 --- a/app/assets/javascripts/leaflet.layers.js +++ b/app/assets/javascripts/leaflet.layers.js @@ -14,7 +14,7 @@ L.OSM.layers = function (options) { var buttonContainer = $("
") .appendTo(baseSection); - var mapContainer = $("
") + var mapContainer = $("
") .appendTo(buttonContainer); var input = $("") diff --git a/app/assets/javascripts/leaflet.map.js b/app/assets/javascripts/leaflet.map.js index d221ae208..78f43b73a 100644 --- a/app/assets/javascripts/leaflet.map.js +++ b/app/assets/javascripts/leaflet.map.js @@ -15,106 +15,24 @@ L.OSM.Map = L.Map.extend({ initialize: function (id, options) { L.Map.prototype.initialize.call(this, id, options); - var copyright_link = $("", { - href: "/copyright", - text: I18n.t("javascripts.map.openstreetmap_contributors") - }).prop("outerHTML"); - var copyright = I18n.t("javascripts.map.copyright_text", { copyright_link: copyright_link }); - - var donate = $("", { - "href": "https://supporting.openstreetmap.org", - "class": "donate-attr", - "text": I18n.t("javascripts.map.make_a_donation") - }).prop("outerHTML"); - - var terms = $("", { - href: "https://wiki.osmfoundation.org/wiki/Terms_of_Use", - text: I18n.t("javascripts.map.website_and_api_terms") - }).prop("outerHTML"); - - var cyclosm_link = $("", { - href: "https://www.cyclosm.org", - target: "_blank", - text: I18n.t("javascripts.map.cyclosm_name") - }).prop("outerHTML"); - var osm_france_link = $("", { - href: "https://openstreetmap.fr/", - target: "_blank", - text: I18n.t("javascripts.map.osm_france") - }).prop("outerHTML"); - var cyclosm = I18n.t("javascripts.map.cyclosm_credit", { cyclosm_link: cyclosm_link, osm_france_link: osm_france_link }); - - var thunderforest_link = $("", { - href: "https://www.thunderforest.com/", - target: "_blank", - text: I18n.t("javascripts.map.andy_allan") - }).prop("outerHTML"); - var thunderforest = I18n.t("javascripts.map.thunderforest_credit", { thunderforest_link: thunderforest_link }); - - var tracestrack_link = $("", { - href: "https://www.tracestrack.com/", - target: "_blank", - text: I18n.t("javascripts.map.tracestrack") - }).prop("outerHTML"); - var tracestrack = I18n.t("javascripts.map.tracestrack_credit", { tracestrack_link: tracestrack_link }); - - var hotosm_link = $("", { - href: "https://www.hotosm.org/", - target: "_blank", - text: I18n.t("javascripts.map.hotosm_name") - }).prop("outerHTML"); - var hotosm = I18n.t("javascripts.map.hotosm_credit", { hotosm_link: hotosm_link, osm_france_link: osm_france_link }); - this.baseLayers = []; - this.baseLayers.push(new L.OSM.Mapnik({ - attribution: copyright + " ♥ " + donate + ". " + terms, - code: "M", - keyid: "mapnik", - name: I18n.t("javascripts.map.base.standard") - })); - - this.baseLayers.push(new L.OSM.CyclOSM({ - attribution: copyright + ". " + cyclosm + ". " + terms, - code: "Y", - keyid: "cyclosm", - name: I18n.t("javascripts.map.base.cyclosm") - })); - - if (OSM.THUNDERFOREST_KEY) { - this.baseLayers.push(new L.OSM.CycleMap({ - attribution: copyright + ". " + thunderforest + ". " + terms, - apikey: OSM.THUNDERFOREST_KEY, - code: "C", - keyid: "cyclemap", - name: I18n.t("javascripts.map.base.cycle_map") - })); - - this.baseLayers.push(new L.OSM.TransportMap({ - attribution: copyright + ". " + thunderforest + ". " + terms, - apikey: OSM.THUNDERFOREST_KEY, - code: "T", - keyid: "transportmap", - name: I18n.t("javascripts.map.base.transport_map") - })); - } + for (const layerDefinition of OSM.LAYER_DEFINITIONS) { + if (layerDefinition.apiKeyId && !OSM[layerDefinition.apiKeyId]) continue; - if (OSM.TRACESTRACK_KEY) { - this.baseLayers.push(new L.OSM.TracestrackTopo({ - attribution: copyright + ". " + tracestrack + ". " + terms, - apikey: OSM.TRACESTRACK_KEY, - code: "P", - keyid: "tracestracktopo", - name: I18n.t("javascripts.map.base.tracestracktop_topo") - })); - } + const layerOptions = { + attribution: makeAttribution(layerDefinition.credit), + code: layerDefinition.code, + keyid: layerDefinition.keyId, + name: I18n.t(`javascripts.map.base.${layerDefinition.nameId}`) + }; + if (layerDefinition.apiKeyId) { + layerOptions.apikey = OSM[layerDefinition.apiKeyId]; + } - this.baseLayers.push(new L.OSM.HOT({ - attribution: copyright + ". " + hotosm + ". " + terms, - code: "H", - keyid: "hot", - name: I18n.t("javascripts.map.base.hot") - })); + const layer = new L.OSM[layerDefinition.leafletOsmId](layerOptions); + this.baseLayers.push(layer); + } this.noteLayer = new L.FeatureGroup(); this.noteLayer.options = { code: "N" }; @@ -124,8 +42,7 @@ L.OSM.Map = L.Map.extend({ this.gpsLayer = new L.OSM.GPS({ pane: "overlayPane", - code: "G", - name: I18n.t("javascripts.map.base.gps") + code: "G" }); this.on("layeradd", function (event) { @@ -133,6 +50,50 @@ L.OSM.Map = L.Map.extend({ this.setMaxZoom(event.layer.options.maxZoom); } }); + + function makeAttribution(credit) { + let attribution = ""; + + attribution += I18n.t("javascripts.map.copyright_text", { + copyright_link: $("", { + href: "/copyright", + text: I18n.t("javascripts.map.openstreetmap_contributors") + }).prop("outerHTML") + }); + + attribution += credit.donate ? " ♥ " : ". "; + attribution += makeCredit(credit); + attribution += ". "; + + attribution += $("", { + href: "https://wiki.osmfoundation.org/wiki/Terms_of_Use", + text: I18n.t("javascripts.map.website_and_api_terms") + }).prop("outerHTML"); + + return attribution; + } + + function makeCredit(credit) { + const children = {}; + for (const childId in credit.children) { + children[childId] = makeCredit(credit.children[childId]); + } + const text = I18n.t(`javascripts.map.${credit.id}`, children); + if (credit.href) { + const link = $("", { + href: credit.href, + text: text + }); + if (credit.donate) { + link.addClass("donate-attr"); + } else { + link.attr("target", "_blank"); + } + return link.prop("outerHTML"); + } else { + return text; + } + } }, updateLayers: function (layerParam) { diff --git a/app/assets/javascripts/osm.js.erb b/app/assets/javascripts/osm.js.erb index e08528f84..e9c09c79f 100644 --- a/app/assets/javascripts/osm.js.erb +++ b/app/assets/javascripts/osm.js.erb @@ -1,5 +1,6 @@ //= depend_on settings.yml //= depend_on settings.local.yml +//= depend_on layers.yml //= depend_on key.yml //= require qs/dist/qs @@ -30,6 +31,7 @@ OSM = { TRACESTRACK_KEY: <%= Settings.tracestrack_key.to_json %>, <% end %> + LAYER_DEFINITIONS: <%= YAML.load_file(Rails.root.join("config/layers.yml")).to_json %>, LAYERS_WITH_MAP_KEY: <%= YAML.load_file(Rails.root.join("config/key.yml")).keys.to_json %>, MARKER_GREEN: <%= image_path("marker-green.png").to_json %>, diff --git a/app/assets/javascripts/richtext.js b/app/assets/javascripts/richtext.js index e069f6f88..bd00d937e 100644 --- a/app/assets/javascripts/richtext.js +++ b/app/assets/javascripts/richtext.js @@ -1,24 +1,22 @@ -$(document).ready(function () { +(function () { /* * When the text in an edit pane is changed, clear the contents of * the associated preview pne so that it will be regenerated when * the user next switches to it. */ - $(".richtext_container textarea").change(function () { - var container = $(this).closest(".richtext_container"); - - container.find(".tab-pane[id$='_preview']").empty(); - }).on("invalid", function () { + $(document).on("change", ".richtext_container textarea", function () { var container = $(this).closest(".richtext_container"); + var preview = container.find(".tab-pane[id$='_preview']"); - container.find("button[data-bs-target$='_edit']").tab("show"); + preview.children(".richtext_placeholder").attr("hidden", true).removeClass("delayed-fade-in"); + preview.children(".richtext").empty(); }); /* * Install a handler to set the minimum preview pane height * when switching away from an edit pane */ - $(".richtext_container button[data-bs-target$='_edit']").on("hide.bs.tab", function () { + $(document).on("hide.bs.tab", ".richtext_container button[data-bs-target$='_edit']", function () { var container = $(this).closest(".richtext_container"); var editor = container.find("textarea"); var preview = container.find(".tab-pane[id$='_preview']"); @@ -30,24 +28,34 @@ $(document).ready(function () { /* * Install a handler to switch to preview mode */ - $(".richtext_container button[data-bs-target$='_preview']").on("show.bs.tab", function () { + $(document).on("show.bs.tab", ".richtext_container button[data-bs-target$='_preview']", function () { var container = $(this).closest(".richtext_container"); var editor = container.find("textarea"); var preview = container.find(".tab-pane[id$='_preview']"); - if (preview.contents().length === 0) { - preview.oneTime(500, "loading", function () { - preview.addClass("loading"); - }); + if (preview.children(".richtext").contents().length === 0) { + preview.children(".richtext_placeholder").removeAttr("hidden").addClass("delayed-fade-in"); - preview.load(editor.data("previewUrl"), { text: editor.val() }, function () { - preview.stopTime("loading"); - preview.removeClass("loading"); + preview.children(".richtext").load(editor.data("previewUrl"), { text: editor.val() }, function () { + preview.children(".richtext_placeholder").attr("hidden", true).removeClass("delayed-fade-in"); }); } }); - var updateHelp = function () { + $(window).on("resize", updateHelp); + + $(document).on("turbo:load", function () { + $(".richtext_container textarea").on("invalid", invalidTextareaListener); + updateHelp(); + }); + + function invalidTextareaListener() { + var container = $(this).closest(".richtext_container"); + + container.find("button[data-bs-target$='_edit']").tab("show"); + } + + function updateHelp() { $(".richtext_container .richtext_help_sidebar:not(:visible):not(:empty)").each(function () { var container = $(this).closest(".richtext_container"); $(this).children().appendTo(container.find(".tab-pane[id$='_help']")); @@ -59,8 +67,5 @@ $(document).ready(function () { container.find("button[data-bs-target$='_edit']").tab("show"); } }); - }; - - updateHelp(); - $(window).on("resize", updateHelp); -}); + } +}()); diff --git a/app/assets/javascripts/router.js b/app/assets/javascripts/router.js index d890f38a4..c4e524170 100644 --- a/app/assets/javascripts/router.js +++ b/app/assets/javascripts/router.js @@ -101,6 +101,16 @@ OSM.Router = function (map, rts) { var router = {}; + function updateSecondaryNav() { + $("header nav.secondary > ul > li > a").each(function () { + var active = $(this).attr("href") === window.location.pathname; + + $(this) + .toggleClass("text-secondary", !active) + .toggleClass("text-secondary-emphasis", active); + }); + } + $(window).on("popstate", function (e) { if (!e.originalEvent.state) return; // Is it a real popstate event or just a hash change? var path = window.location.pathname + window.location.search, @@ -110,6 +120,7 @@ OSM.Router = function (map, rts) { currentPath = path; currentRoute = route; currentRoute.run("popstate", currentPath); + updateSecondaryNav(); map.setState(e.originalEvent.state, { animate: false }); }); @@ -124,6 +135,7 @@ OSM.Router = function (map, rts) { currentPath = path; currentRoute = route; currentRoute.run("pushstate", currentPath); + updateSecondaryNav(); return true; }; diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js index 495470f2f..b94db8b55 100644 --- a/app/assets/javascripts/user.js +++ b/app/assets/javascripts/user.js @@ -1,4 +1,10 @@ -//= require leaflet.locatecontrol/src/L.Control.Locate +//= require leaflet.locatecontrol/dist/L.Control.Locate.umd + +(function () { + $(document).on("change", "#user_all", function () { + $("#user_list input[type=checkbox]").prop("checked", $("#user_all").prop("checked")); + }); +}()); $(document).ready(function () { var defaultHomeZoom = 12; @@ -200,10 +206,6 @@ $(document).ready(function () { enableAuth(); } - $("#user_all").change(function () { - $("#user_list input[type=checkbox]").prop("checked", $("#user_all").prop("checked")); - }); - $("#content.user_confirm").each(function () { $(this).hide(); $(this).find("#confirm").submit(); diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index d551462b2..4bedf42b6 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -70,6 +70,28 @@ time[title] { } } +/* Utility for delayed loading spinner */ + +.delayed-fade-in { + animation: 300ms linear forwards delayed-fade-in; +} + +@keyframes delayed-fade-in { + 0% { opacity: 0 } + 66% { opacity: 0 } + 100% { opacity: 1 } +} + +/* Bootstrap close button overrides for nested light/dark themes */ + +[data-bs-theme="dark"] .btn-close { + filter: var(--bs-btn-close-white-filter); +} + +[data-bs-theme="light"] .btn-close { + filter: none; +} + /* Rules for the header */ #menu-icon { @@ -119,7 +141,7 @@ header { } nav.primary { - & > .btn-group .btn-outline-primary { + #edit_tab .btn-outline-primary { @include button-outline-variant($green, $color-hover: $white, $active-color: $white); } @@ -197,9 +219,7 @@ body.small-nav { } } - #sidebar .search_forms, - #edit_tab, - #export_tab { + #sidebar .search_forms { display: none; } @@ -207,7 +227,7 @@ body.small-nav { margin-right: 0; padding: 0; - .btn-group { + #edit_tab { width: 100%; padding: 10px; } @@ -368,6 +388,14 @@ body.small-nav { .leaflet-marker-draggable { cursor: move; } + + .query-marker { + animation: 1500ms forwards query-marker-fade; + + @keyframes query-marker-fade { + to { opacity: 0 } + } + } } #map-ui { @@ -474,17 +502,17 @@ body.small-nav { } @include color-mode(dark) { - .leaflet-tile-container, + .leaflet-tile-container .leaflet-tile, .mapkey-table-entry td:first-child > * { filter: brightness(.8); } - .leaflet-control-attribution a { + .leaflet-container .leaflet-control-attribution a { color: var(--bs-link-color); } .leaflet-control-scale-line { - @extend .border-light, .border-opacity-75; + border-color: rgba(var(--bs-light-rgb), .75) !important; } } @@ -945,6 +973,10 @@ img.trace_image { .node, .way, .relation { margin-left: 25px; } + + .node::before { content: image-url('browse/node.svg'); } + .way::before { content: image-url('browse/way.svg'); } + .relation::before { content: image-url('browse/relation.svg'); } } @each $class, $item in $map-sidebar-icons { diff --git a/app/assets/stylesheets/parameters.scss b/app/assets/stylesheets/parameters.scss index 07549d69b..28bf56901 100644 --- a/app/assets/stylesheets/parameters.scss +++ b/app/assets/stylesheets/parameters.scss @@ -20,3 +20,4 @@ $table-border-factor: .1; $list-group-hover-bg: rgba(var(--bs-emphasis-color-rgb), .075); $enable-negative-margins: true; +$color-mode-type: media-query; diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index d45dce66a..085d1bfea 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -16,8 +16,6 @@ class AccountsController < ApplicationController allow_social_login :only => [:edit, :update] def edit - @tokens = current_user.oauth_tokens.authorized - if errors = session.delete(:user_errors) errors.each do |attribute, error| current_user.errors.add(attribute, error) @@ -27,8 +25,6 @@ class AccountsController < ApplicationController end def update - @tokens = current_user.oauth_tokens.authorized - user_params = params.require(:user).permit(:display_name, :new_email, :pass_crypt, :pass_crypt_confirmation, :auth_provider) if params[:user][:auth_provider].blank? || diff --git a/app/controllers/api/capabilities_controller.rb b/app/controllers/api/capabilities_controller.rb index cbdcace0c..b0600ca33 100644 --- a/app/controllers/api/capabilities_controller.rb +++ b/app/controllers/api/capabilities_controller.rb @@ -5,7 +5,6 @@ module Api authorize_resource :class => false before_action :set_request_formats - around_action :api_call_handle_error, :api_call_timeout # External apps that use the api are able to query the api to find out some # parameters of the API. It currently returns: diff --git a/app/controllers/api/changeset_comments_controller.rb b/app/controllers/api/changeset_comments_controller.rb index 4a96ec3bb..c180571c5 100644 --- a/app/controllers/api/changeset_comments_controller.rb +++ b/app/controllers/api/changeset_comments_controller.rb @@ -6,9 +6,8 @@ module Api authorize_resource before_action :require_public_data, :only => [:create] + before_action :set_request_formats - around_action :api_call_handle_error - around_action :api_call_timeout ## # Add a comment to a changeset diff --git a/app/controllers/api/changesets_controller.rb b/app/controllers/api/changesets_controller.rb index 3d59eeb17..9111bb609 100644 --- a/app/controllers/api/changesets_controller.rb +++ b/app/controllers/api/changesets_controller.rb @@ -11,8 +11,7 @@ module Api before_action :require_public_data, :only => [:create, :update, :upload, :close, :subscribe, :unsubscribe] before_action :set_request_formats, :except => [:create, :close, :upload] - around_action :api_call_handle_error - around_action :api_call_timeout, :except => [:upload] + skip_around_action :api_call_timeout, :only => [:upload] # Helper methods for checking consistency include ConsistencyValidations diff --git a/app/controllers/api/map_controller.rb b/app/controllers/api/map_controller.rb index 6d4a9feb6..da8138597 100644 --- a/app/controllers/api/map_controller.rb +++ b/app/controllers/api/map_controller.rb @@ -2,8 +2,6 @@ module Api class MapController < ApiController authorize_resource :class => false - around_action :api_call_handle_error, :api_call_timeout - before_action :set_request_formats # This is probably the most common call of all. It is used for getting the diff --git a/app/controllers/api/messages_controller.rb b/app/controllers/api/messages_controller.rb index 074f87398..886922bff 100644 --- a/app/controllers/api/messages_controller.rb +++ b/app/controllers/api/messages_controller.rb @@ -9,8 +9,6 @@ module Api authorize_resource - around_action :api_call_handle_error, :api_call_timeout - before_action :set_request_formats def inbox diff --git a/app/controllers/api/nodes_controller.rb b/app/controllers/api/nodes_controller.rb index 5aad78dbf..6477271d4 100644 --- a/app/controllers/api/nodes_controller.rb +++ b/app/controllers/api/nodes_controller.rb @@ -8,8 +8,6 @@ module Api authorize_resource before_action :require_public_data, :only => [:create, :update, :delete] - around_action :api_call_handle_error, :api_call_timeout - before_action :set_request_formats, :except => [:create, :update, :delete] before_action :check_rate_limit, :only => [:create, :update, :delete] diff --git a/app/controllers/api/note_subscriptions_controller.rb b/app/controllers/api/note_subscriptions_controller.rb new file mode 100644 index 000000000..c416dd803 --- /dev/null +++ b/app/controllers/api/note_subscriptions_controller.rb @@ -0,0 +1,27 @@ +module Api + class NoteSubscriptionsController < ApiController + before_action :check_api_writable + before_action :authorize + + authorize_resource + + def create + note_id = params[:note_id].to_i + note = Note.find(note_id) + note.subscribers << current_user + rescue ActiveRecord::RecordNotFound + report_error "Note #{note_id} not found.", :not_found + rescue ActiveRecord::RecordNotUnique + report_error "You are already subscribed to note #{note_id}.", :conflict + end + + def destroy + note_id = params[:note_id].to_i + note = Note.find(note_id) + count = note.subscriptions.where(:user => current_user).delete_all + report_error "You are not subscribed to note #{note_id}.", :not_found if count.zero? + rescue ActiveRecord::RecordNotFound + report_error "Note #{note_id} not found.", :not_found + end + end +end diff --git a/app/controllers/api/notes_controller.rb b/app/controllers/api/notes_controller.rb index be36421d9..7e2e7fb79 100644 --- a/app/controllers/api/notes_controller.rb +++ b/app/controllers/api/notes_controller.rb @@ -7,7 +7,6 @@ module Api authorize_resource before_action :set_locale - around_action :api_call_handle_error, :api_call_timeout before_action :set_request_formats, :except => [:feed] ## @@ -385,7 +384,7 @@ module Api def add_comment(note, text, event, notify: true) attributes = { :visible => true, :event => event, :body => text } - if doorkeeper_token || current_token + if doorkeeper_token author = current_user if scope_enabled?(:write_notes) else author = current_user @@ -399,9 +398,13 @@ module Api comment = note.comments.create!(attributes) - note.comments.map(&:author).uniq.each do |user| - UserMailer.note_comment_notification(comment, user).deliver_later if notify && user && user != current_user && user.visible? + if notify + note.subscribers.visible.each do |user| + UserMailer.note_comment_notification(comment, user).deliver_later if current_user != user + end end + + NoteSubscription.find_or_create_by(:note => note, :user => current_user) if current_user end end end diff --git a/app/controllers/api/old_elements_controller.rb b/app/controllers/api/old_elements_controller.rb index 2343252db..73e57c1f8 100644 --- a/app/controllers/api/old_elements_controller.rb +++ b/app/controllers/api/old_elements_controller.rb @@ -9,7 +9,6 @@ module Api authorize_resource - around_action :api_call_handle_error, :api_call_timeout before_action :lookup_old_element, :except => [:history] before_action :lookup_old_element_versions, :only => [:history] diff --git a/app/controllers/api/permissions_controller.rb b/app/controllers/api/permissions_controller.rb index 717bbfa6f..75bfe6b10 100644 --- a/app/controllers/api/permissions_controller.rb +++ b/app/controllers/api/permissions_controller.rb @@ -4,20 +4,14 @@ module Api before_action :setup_user_auth before_action :set_request_formats - around_action :api_call_handle_error, :api_call_timeout # External apps that use the api are able to query which permissions # they have. This currently returns a list of permissions granted to the current user: # * if authenticated via OAuth, this list will contain all permissions granted by the user to the access_token. - # * if authenticated via basic auth all permissions are granted, so the list will contain all permissions. # * unauthenticated users have no permissions, so the list will be empty. def show @permissions = if doorkeeper_token.present? doorkeeper_token.scopes.map { |s| :"allow_#{s}" } - elsif current_token.present? - ClientApplication.all_permissions.select { |p| current_token.read_attribute(p) } - elsif current_user - ClientApplication.all_permissions else [] end diff --git a/app/controllers/api/relations_controller.rb b/app/controllers/api/relations_controller.rb index 5fb99dbd1..b237f8bf8 100644 --- a/app/controllers/api/relations_controller.rb +++ b/app/controllers/api/relations_controller.rb @@ -6,8 +6,6 @@ module Api authorize_resource before_action :require_public_data, :only => [:create, :update, :delete] - around_action :api_call_handle_error, :api_call_timeout - before_action :set_request_formats, :except => [:create, :update, :delete] before_action :check_rate_limit, :only => [:create, :update, :delete] diff --git a/app/controllers/api/tracepoints_controller.rb b/app/controllers/api/tracepoints_controller.rb index d8d9da98b..e45b5968c 100644 --- a/app/controllers/api/tracepoints_controller.rb +++ b/app/controllers/api/tracepoints_controller.rb @@ -2,8 +2,6 @@ module Api class TracepointsController < ApiController authorize_resource - around_action :api_call_handle_error, :api_call_timeout - # Get an XML response containing a list of tracepoints that have been uploaded # within the specified bounding box, and in the specified page. def index diff --git a/app/controllers/api/traces_controller.rb b/app/controllers/api/traces_controller.rb index 738642fff..76dfb3a2d 100644 --- a/app/controllers/api/traces_controller.rb +++ b/app/controllers/api/traces_controller.rb @@ -7,7 +7,7 @@ module Api authorize_resource before_action :offline_error, :only => [:create, :destroy, :data] - around_action :api_call_handle_error + skip_around_action :api_call_timeout, :only => :create def show @trace = Trace.visible.find(params[:id]) diff --git a/app/controllers/api/user_blocks_controller.rb b/app/controllers/api/user_blocks_controller.rb index 6c285e14a..51f0d26d3 100644 --- a/app/controllers/api/user_blocks_controller.rb +++ b/app/controllers/api/user_blocks_controller.rb @@ -2,7 +2,6 @@ module Api class UserBlocksController < ApiController authorize_resource - around_action :api_call_handle_error, :api_call_timeout before_action :set_request_formats def show diff --git a/app/controllers/api/user_preferences_controller.rb b/app/controllers/api/user_preferences_controller.rb index cb852ce88..d1bd6d624 100644 --- a/app/controllers/api/user_preferences_controller.rb +++ b/app/controllers/api/user_preferences_controller.rb @@ -6,8 +6,6 @@ module Api authorize_resource - around_action :api_call_handle_error - before_action :set_request_formats ## diff --git a/app/controllers/api/users_controller.rb b/app/controllers/api/users_controller.rb index 5ff275ee9..e9f42e12b 100644 --- a/app/controllers/api/users_controller.rb +++ b/app/controllers/api/users_controller.rb @@ -6,7 +6,6 @@ module Api authorize_resource - around_action :api_call_handle_error load_resource :only => :show before_action :set_request_formats, :except => [:gpx_files] diff --git a/app/controllers/api/versions_controller.rb b/app/controllers/api/versions_controller.rb index d311a18d2..15cde9d6d 100644 --- a/app/controllers/api/versions_controller.rb +++ b/app/controllers/api/versions_controller.rb @@ -4,7 +4,6 @@ module Api authorize_resource :class => false before_action :set_request_formats - around_action :api_call_handle_error, :api_call_timeout # Show the list of available API versions. This will replace the global # unversioned capabilities call in due course. diff --git a/app/controllers/api/ways_controller.rb b/app/controllers/api/ways_controller.rb index 4099e1676..27c4a1fcc 100644 --- a/app/controllers/api/ways_controller.rb +++ b/app/controllers/api/ways_controller.rb @@ -6,8 +6,6 @@ module Api authorize_resource before_action :require_public_data, :only => [:create, :update, :delete] - around_action :api_call_handle_error, :api_call_timeout - before_action :set_request_formats, :except => [:create, :update, :delete] before_action :check_rate_limit, :only => [:create, :update, :delete] diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index ae1bc8755..17c98fe8b 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -3,6 +3,8 @@ class ApiController < ApplicationController before_action :check_api_readable + around_action :api_call_handle_error, :api_call_timeout + private ## @@ -47,19 +49,14 @@ class ApiController < ApplicationController end end - def authorize(realm = "Web Password", errormessage = "Couldn't authenticate you") + def authorize(errormessage = "Couldn't authenticate you") # make the current_user object from any auth sources we have setup_user_auth # handle authenticate pass/fail unless current_user # no auth, the user does not exist or the password was wrong - if Settings.basic_auth_support - response.headers["WWW-Authenticate"] = "Basic realm=\"#{realm}\"" - render :plain => errormessage, :status => :unauthorized - else - render :plain => errormessage, :status => :forbidden - end + render :plain => errormessage, :status => :unauthorized false end @@ -69,26 +66,19 @@ class ApiController < ApplicationController # Use capabilities from the oauth token if it exists and is a valid access token if doorkeeper_token&.accessible? ApiAbility.new(nil).merge(ApiCapability.new(doorkeeper_token)) - elsif Authenticator.new(self, [:token]).allow? - ApiAbility.new(nil).merge(ApiCapability.new(current_token)) else ApiAbility.new(current_user) end end def deny_access(_exception) - if doorkeeper_token || current_token + if doorkeeper_token set_locale report_error t("oauth.permissions.missing"), :forbidden elsif current_user head :forbidden - elsif Settings.basic_auth_support - realm = "Web Password" - errormessage = "Couldn't authenticate you" - response.headers["WWW-Authenticate"] = "Basic realm=\"#{realm}\"" - render :plain => errormessage, :status => :unauthorized else - render :plain => errormessage, :status => :forbidden + head :unauthorized end end @@ -105,32 +95,7 @@ class ApiController < ApplicationController def setup_user_auth logger.info " setup_user_auth" # try and setup using OAuth - if doorkeeper_token&.accessible? - self.current_user = User.find(doorkeeper_token.resource_owner_id) - elsif Authenticator.new(self, [:token]).allow? - if Settings.oauth_10a_support - # self.current_user setup by OAuth - else - report_error t("application.oauth_10a_disabled", :link => t("application.auth_disabled_link")), :forbidden - self.current_user = nil - end - else - username, passwd = auth_data # parse from headers - # authenticate per-scheme - self.current_user = if username.nil? - nil # no authentication provided - perhaps first connect (client should retry after 401) - else - User.authenticate(:username => username, :password => passwd) # basic auth - end - if username && current_user - if Settings.basic_auth_support - # log if we have authenticated using basic auth - logger.info "Authenticated as user #{current_user.id} using basic authentication" - else - report_error t("application.basic_auth_disabled", :link => t("application.auth_disabled_link")), :forbidden - end - end - end + self.current_user = User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token&.accessible? # have we identified the user? if current_user @@ -169,7 +134,7 @@ class ApiController < ApplicationController report_error message, :bad_request rescue OSM::APIError => e report_error e.message, e.status - rescue AbstractController::ActionNotFound => e + rescue AbstractController::ActionNotFound, CanCan::AccessDenied => e raise rescue StandardError => e logger.info("API threw unexpected #{e.class} exception: #{e.message}") @@ -179,8 +144,8 @@ class ApiController < ApplicationController ## # wrap an api call in a timeout - def api_call_timeout(&block) - Timeout.timeout(Settings.api_timeout, &block) + def api_call_timeout(&) + Timeout.timeout(Settings.api_timeout, &) rescue ActionView::Template::Error => e e = e.cause diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c6223fb9d..7ce804ced 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -86,10 +86,6 @@ class ApplicationController < ActionController::Base @oauth_token = current_user.oauth_token(Settings.oauth_application) if current_user && Settings.key?(:oauth_application) end - def require_oauth_10a_support - report_error t("application.oauth_10a_disabled", :link => t("application.auth_disabled_link")), :forbidden unless Settings.oauth_10a_support - end - ## # require the user to have cookies enabled in their browser def require_cookies @@ -219,21 +215,26 @@ class ApplicationController < ActionController::Base ## # wrap a web page in a timeout - def web_timeout(&block) - Timeout.timeout(Settings.web_timeout, &block) + def web_timeout(&) + raise Timeout::Error if Settings.web_timeout.negative? + + Timeout.timeout(Settings.web_timeout, &) rescue ActionView::Template::Error => e e = e.cause if e.is_a?(Timeout::Error) || (e.is_a?(ActiveRecord::StatementInvalid) && e.message.include?("execution expired")) - ActiveRecord::Base.connection.raw_connection.cancel - render :action => "timeout" + respond_to_timeout else raise end rescue Timeout::Error + respond_to_timeout + end + + def respond_to_timeout ActiveRecord::Base.connection.raw_connection.cancel - render :action => "timeout" + render :action => "timeout", :status => :gateway_timeout end ## @@ -297,7 +298,7 @@ class ApplicationController < ActionController::Base end def deny_access(_exception) - if doorkeeper_token || current_token + if doorkeeper_token set_locale report_error t("oauth.permissions.missing"), :forbidden elsif current_user @@ -327,23 +328,6 @@ class ApplicationController < ActionController::Base end end - # extract authorisation credentials from headers, returns user = nil if none - def auth_data - if request.env.key? "X-HTTP_AUTHORIZATION" # where mod_rewrite might have put it - authdata = request.env["X-HTTP_AUTHORIZATION"].to_s.split - elsif request.env.key? "REDIRECT_X_HTTP_AUTHORIZATION" # mod_fcgi - authdata = request.env["REDIRECT_X_HTTP_AUTHORIZATION"].to_s.split - elsif request.env.key? "HTTP_AUTHORIZATION" # regular location - authdata = request.env["HTTP_AUTHORIZATION"].to_s.split - end - # only basic authentication supported - user, pass = Base64.decode64(authdata[1]).split(":", 2) if authdata && authdata[0] == "Basic" - [user, pass] - end - - # override to stop oauth plugin sending errors - def invalid_oauth_response; end - # clean any referer parameter def safe_referer(referer) begin @@ -366,7 +350,7 @@ class ApplicationController < ActionController::Base end def scope_enabled?(scope) - doorkeeper_token&.includes_scope?(scope) || current_token&.includes_scope?(scope) + doorkeeper_token&.includes_scope?(scope) end helper_method :scope_enabled? diff --git a/app/controllers/changeset_comments/feeds_controller.rb b/app/controllers/changeset_comments/feeds_controller.rb new file mode 100644 index 000000000..fef48bb18 --- /dev/null +++ b/app/controllers/changeset_comments/feeds_controller.rb @@ -0,0 +1,52 @@ +module ChangesetComments + class FeedsController < ApplicationController + before_action :authorize_web + before_action :set_locale + + authorize_resource :changeset_comment + + before_action -> { check_database_readable(:need_api => true) } + around_action :web_timeout + + ## + # Get a feed of recent changeset comments + def show + if params[:changeset_id] + # Extract the arguments + changeset_id = params[:changeset_id].to_i + + # Find the changeset + changeset = Changeset.find(changeset_id) + + # Return comments for this changeset only + @comments = changeset.comments.includes(:author, :changeset).reverse_order.limit(comments_limit) + else + # Return comments + @comments = ChangesetComment.includes(:author, :changeset).where(:visible => true).order("created_at DESC").limit(comments_limit).preload(:changeset) + end + + # Render the result + respond_to do |format| + format.rss + end + rescue OSM::APIBadUserInput + head :bad_request + end + + private + + ## + # Get the maximum number of comments to return + def comments_limit + if params[:limit] + if params[:limit].to_i.positive? && params[:limit].to_i <= 10000 + params[:limit].to_i + else + raise OSM::APIBadUserInput, "Comments limit must be between 1 and 10000" + end + else + 100 + end + end + end +end diff --git a/app/controllers/changeset_comments_controller.rb b/app/controllers/changeset_comments_controller.rb deleted file mode 100644 index 637ac7be6..000000000 --- a/app/controllers/changeset_comments_controller.rb +++ /dev/null @@ -1,50 +0,0 @@ -class ChangesetCommentsController < ApplicationController - before_action :authorize_web - before_action :set_locale - - authorize_resource - - before_action -> { check_database_readable(:need_api => true) } - around_action :web_timeout - - ## - # Get a feed of recent changeset comments - def index - if params[:id] - # Extract the arguments - id = params[:id].to_i - - # Find the changeset - changeset = Changeset.find(id) - - # Return comments for this changeset only - @comments = changeset.comments.includes(:author, :changeset).reverse_order.limit(comments_limit) - else - # Return comments - @comments = ChangesetComment.includes(:author, :changeset).where(:visible => true).order("created_at DESC").limit(comments_limit).preload(:changeset) - end - - # Render the result - respond_to do |format| - format.rss - end - rescue OSM::APIBadUserInput - head :bad_request - end - - private - - ## - # Get the maximum number of comments to return - def comments_limit - if params[:limit] - if params[:limit].to_i.positive? && params[:limit].to_i <= 10000 - params[:limit].to_i - else - raise OSM::APIBadUserInput, "Comments limit must be between 1 and 10000" - end - else - 100 - end - end -end diff --git a/app/controllers/changesets_controller.rb b/app/controllers/changesets_controller.rb index a5ddaf364..928f1c1ec 100644 --- a/app/controllers/changesets_controller.rb +++ b/app/controllers/changesets_controller.rb @@ -77,20 +77,32 @@ class ChangesetsController < ApplicationController def show @type = "changeset" @changeset = Changeset.find(params[:id]) - @comments = if current_user&.moderator? - @changeset.comments.unscope(:where => :visible).includes(:author) - else - @changeset.comments.includes(:author) - end - @node_pages, @nodes = paginate(:old_nodes, :conditions => { :changeset_id => @changeset.id }, :per_page => 20, :parameter => "node_page") - @way_pages, @ways = paginate(:old_ways, :conditions => { :changeset_id => @changeset.id }, :per_page => 20, :parameter => "way_page") - @relation_pages, @relations = paginate(:old_relations, :conditions => { :changeset_id => @changeset.id }, :per_page => 20, :parameter => "relation_page") - if @changeset.user.active? && @changeset.user.data_public? - changesets = conditions_nonempty(@changeset.user.changesets) - @next_by_user = changesets.where("id > ?", @changeset.id).reorder(:id => :asc).first - @prev_by_user = changesets.where(:id => ...@changeset.id).reorder(:id => :desc).first + case turbo_frame_request_id + when "changeset_nodes" + @node_pages, @nodes = paginate(:old_nodes, :conditions => { :changeset_id => @changeset.id }, :order => [:node_id, :version], :per_page => 20, :parameter => "node_page") + render :partial => "elements", :locals => { :type => "node", :elements => @nodes, :pages => @node_pages } + when "changeset_ways" + @way_pages, @ways = paginate(:old_ways, :conditions => { :changeset_id => @changeset.id }, :order => [:way_id, :version], :per_page => 20, :parameter => "way_page") + render :partial => "elements", :locals => { :type => "way", :elements => @ways, :pages => @way_pages } + when "changeset_relations" + @relation_pages, @relations = paginate(:old_relations, :conditions => { :changeset_id => @changeset.id }, :order => [:relation_id, :version], :per_page => 20, :parameter => "relation_page") + render :partial => "elements", :locals => { :type => "relation", :elements => @relations, :pages => @relation_pages } + else + @comments = if current_user&.moderator? + @changeset.comments.unscope(:where => :visible).includes(:author) + else + @changeset.comments.includes(:author) + end + @node_pages, @nodes = paginate(:old_nodes, :conditions => { :changeset_id => @changeset.id }, :order => [:node_id, :version], :per_page => 20, :parameter => "node_page") + @way_pages, @ways = paginate(:old_ways, :conditions => { :changeset_id => @changeset.id }, :order => [:way_id, :version], :per_page => 20, :parameter => "way_page") + @relation_pages, @relations = paginate(:old_relations, :conditions => { :changeset_id => @changeset.id }, :order => [:relation_id, :version], :per_page => 20, :parameter => "relation_page") + if @changeset.user.active? && @changeset.user.data_public? + changesets = conditions_nonempty(@changeset.user.changesets) + @next_by_user = changesets.where("id > ?", @changeset.id).reorder(:id => :asc).first + @prev_by_user = changesets.where(:id => ...@changeset.id).reorder(:id => :desc).first + end + render :layout => map_layout end - render :layout => map_layout rescue ActiveRecord::RecordNotFound render :template => "browse/not_found", :status => :not_found, :layout => map_layout end diff --git a/app/controllers/concerns/user_methods.rb b/app/controllers/concerns/user_methods.rb index 28305b5c3..d79ed48d2 100644 --- a/app/controllers/concerns/user_methods.rb +++ b/app/controllers/concerns/user_methods.rb @@ -6,9 +6,10 @@ module UserMethods ## # ensure that there is a "user" instance variable def lookup_user - @user = User.active.find_by!(:display_name => params[:display_name]) + display_name = params[:display_name] || params[:user_display_name] + @user = User.active.find_by!(:display_name => display_name) rescue ActiveRecord::RecordNotFound - render_unknown_user params[:display_name] + render_unknown_user display_name end ## diff --git a/app/controllers/diary_entries_controller.rb b/app/controllers/diary_entries_controller.rb index ff6dfc826..760c9a301 100644 --- a/app/controllers/diary_entries_controller.rb +++ b/app/controllers/diary_entries_controller.rb @@ -71,6 +71,7 @@ class DiaryEntriesController < ApplicationController if @entry @title = t ".title", :user => params[:display_name], :title => @entry.title @opengraph_properties = { + "og:title" => @entry.title, "og:image" => @entry.body.image, "og:image:alt" => @entry.body.image_alt, "og:description" => @entry.body.description, diff --git a/app/controllers/friendships_controller.rb b/app/controllers/friendships_controller.rb index ab54cbfd1..8f0c1ad85 100644 --- a/app/controllers/friendships_controller.rb +++ b/app/controllers/friendships_controller.rb @@ -20,7 +20,7 @@ class FriendshipsController < ApplicationController if current_user.friends_with?(@friend) flash[:warning] = t ".already_a_friend", :name => @friend.display_name elsif current_user.friendships.where(:created_at => Time.now.utc - 1.hour..).count >= current_user.max_friends_per_hour - flash.now[:error] = t ".limit_exceeded" + flash[:error] = t ".limit_exceeded" elsif friendship.save flash[:notice] = t ".success", :name => @friend.display_name UserMailer.friendship_notification(friendship).deliver_later diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb index c24054f77..fe900d627 100644 --- a/app/controllers/issues_controller.rb +++ b/app/controllers/issues_controller.rb @@ -1,4 +1,6 @@ class IssuesController < ApplicationController + include PaginationMethods + layout "site" before_action :authorize_web @@ -11,6 +13,8 @@ class IssuesController < ApplicationController before_action :check_database_writable, :only => [:resolve, :ignore, :reopen] def index + @params = params.permit(:before, :after, :limit, :status, :search_by_user, :issue_type, :last_updated_by) + @params[:limit] ||= 50 @title = t ".title" @issue_types = [] @@ -18,17 +22,16 @@ class IssuesController < ApplicationController @issue_types.push("DiaryEntry", "DiaryComment", "User") if current_user.administrator? @users = User.joins(:roles).where(:user_roles => { :role => current_user.roles.map(&:role) }).distinct - @issues = Issue.visible_to(current_user).order(:updated_at => :desc) + @issues = Issue.visible_to(current_user) # If search if params[:search_by_user].present? @find_user = User.find_by(:display_name => params[:search_by_user]) - if @find_user - @issues = @issues.where(:reported_user => @find_user) - else - @issues = @issues.none - flash.now[:warning] = t(".user_not_found") - end + @issues = if @find_user + @issues.where(:reported_user => @find_user) + else + @issues.none + end end @issues = @issues.where(:status => params[:status]) if params[:status].present? @@ -39,6 +42,9 @@ class IssuesController < ApplicationController last_updated_by = params[:last_updated_by].to_s == "nil" ? nil : params[:last_updated_by].to_i @issues = @issues.where(:updated_by => last_updated_by) end + + @issues, @newer_issues_id, @older_issues_id = get_page_items(@issues, :limit => @params[:limit]) + render :partial => "page" if turbo_frame_request_id == "pagination" end def show diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index 658c43483..7d86796b1 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -88,6 +88,16 @@ class MessagesController < ApplicationController @title = @message.title + render :action => "new" + elsif message.sender == current_user + @message = Message.new( + :recipient => message.recipient, + :title => "Re: #{message.title.sub(/^Re:\s*/, '')}", + :body => "On #{message.sent_on} #{message.sender.display_name} wrote:\n\n#{message.body.gsub(/^/, '> ')}" + ) + + @title = @message.title + render :action => "new" else flash[:notice] = t ".wrong_user", :user => current_user.display_name diff --git a/app/controllers/notes_controller.rb b/app/controllers/notes_controller.rb index 26d27692e..c47a3abfb 100644 --- a/app/controllers/notes_controller.rb +++ b/app/controllers/notes_controller.rb @@ -18,12 +18,13 @@ class NotesController < ApplicationController def index param! :page, Integer, :min => 1 - @params = params.permit(:display_name) + @params = params.permit(:display_name, :status) @title = t ".title", :user => @user.display_name @page = (params[:page] || 1).to_i @page_size = 10 @notes = @user.notes @notes = @notes.visible unless current_user&.moderator? + @notes = @notes.where(:status => params[:status]) unless params[:status] == "all" || params[:status].blank? @notes = @notes.order("updated_at DESC, id").distinct.offset((@page - 1) * @page_size).limit(@page_size).preload(:comments => :author) render :layout => "site" diff --git a/app/controllers/oauth_clients_controller.rb b/app/controllers/oauth_clients_controller.rb deleted file mode 100644 index 42b0921f1..000000000 --- a/app/controllers/oauth_clients_controller.rb +++ /dev/null @@ -1,75 +0,0 @@ -class OauthClientsController < ApplicationController - layout "site" - - before_action :authorize_web - before_action :set_locale - - authorize_resource :class => ClientApplication - - def index - @client_applications = current_user.client_applications - @tokens = current_user.oauth_tokens.authorized - end - - def show - @client_application = current_user.client_applications.find(params[:id]) - rescue ActiveRecord::RecordNotFound - @type = "client application" - render :action => "not_found", :status => :not_found - end - - def new - if Settings.oauth_10_registration - @client_application = ClientApplication.new - else - flash[:error] = t ".disabled" - redirect_to :action => "index" - end - end - - def edit - @client_application = current_user.client_applications.find(params[:id]) - rescue ActiveRecord::RecordNotFound - @type = "client application" - render :action => "not_found", :status => :not_found - end - - def create - @client_application = current_user.client_applications.build(application_params) - if @client_application.save - flash[:notice] = t ".flash" - redirect_to :action => "show", :id => @client_application.id - else - render :action => "new" - end - end - - def update - @client_application = current_user.client_applications.find(params[:id]) - if @client_application.update(application_params) - flash[:notice] = t ".flash" - redirect_to :action => "show", :id => @client_application.id - else - render :action => "edit" - end - rescue ActiveRecord::RecordNotFound - @type = "client application" - render :action => "not_found", :status => :not_found - end - - def destroy - @client_application = current_user.client_applications.find(params[:id]) - @client_application.destroy - flash[:notice] = t ".flash" - redirect_to :action => "index" - rescue ActiveRecord::RecordNotFound - @type = "client application" - render :action => "not_found", :status => :not_found - end - - private - - def application_params - params.require(:client_application).permit(:name, :url, :callback_url, :support_url, ClientApplication.all_permissions) - end -end diff --git a/app/controllers/oauth_controller.rb b/app/controllers/oauth_controller.rb deleted file mode 100644 index 62a68b533..000000000 --- a/app/controllers/oauth_controller.rb +++ /dev/null @@ -1,80 +0,0 @@ -class OauthController < ApplicationController - include OAuth::Controllers::ProviderController - - # The ProviderController will call login_required for any action that needs - # a login, but we want to check authorization on every action. - authorize_resource :class => false - - before_action :require_oauth_10a_support - - layout "site" - - allow_all_form_action :only => :oauth1_authorize - - def revoke - @token = current_user.oauth_tokens.find_by :token => params[:token] - if @token - @token.invalidate! - flash[:notice] = t(".flash", :application => @token.client_application.name) - end - redirect_to oauth_clients_url(:display_name => @token.user.display_name) - end - - protected - - def login_required - authorize_web - set_locale - end - - def user_authorizes_token? - any_auth = false - - @token.client_application.permissions.each do |pref| - if params[pref].to_i.nonzero? - @token.write_attribute(pref, true) - any_auth ||= true - else - @token.write_attribute(pref, false) - end - end - - any_auth - end - - def oauth1_authorize - if @token.invalidated? - @message = t "oauth.authorize_failure.invalid" - render :action => "authorize_failure" - elsif request.post? - if user_authorizes_token? - @token.authorize!(current_user) - callback_url = if @token.oauth10? - params[:oauth_callback] || @token.client_application.callback_url - else - @token.oob? ? @token.client_application.callback_url : @token.callback_url - end - @redirect_url = URI.parse(callback_url) if callback_url.present? - - if @redirect_url.to_s.blank? - render :action => "authorize_success" - else - @redirect_url.query = if @redirect_url.query.blank? - "oauth_token=#{@token.token}" - else - @redirect_url.query + - "&oauth_token=#{@token.token}" - end - - @redirect_url.query += "&oauth_verifier=#{@token.verifier}" unless @token.oauth10? - - redirect_to @redirect_url.to_s, :allow_other_host => true - end - else - @token.invalidate! - @message = t("oauth.authorize_failure.denied", :app_name => @token.client_application.name) - render :action => "authorize_failure" - end - end - end -end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index a3e6f42f0..abbaf5e92 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -20,7 +20,7 @@ class SessionsController < ApplicationController end def create - session[:remember_me] ||= params[:remember_me] + session[:remember_me] = params[:remember_me] == "yes" referer = safe_referer(params[:referer]) if params[:referer] diff --git a/app/controllers/traces_controller.rb b/app/controllers/traces_controller.rb index 02b553d0a..d723bac5b 100644 --- a/app/controllers/traces_controller.rb +++ b/app/controllers/traces_controller.rb @@ -143,7 +143,7 @@ class TracesController < ApplicationController flash[:notice] = t ".updated" redirect_to :action => "show", :display_name => current_user.display_name else - @title = t ".title", :name => @trace.name + @title = t "traces.edit.title", :name => @trace.name render :action => "edit" end rescue ActiveRecord::RecordNotFound diff --git a/app/controllers/user_blocks_controller.rb b/app/controllers/user_blocks_controller.rb index 6bf86de3f..d427e5fa5 100644 --- a/app/controllers/user_blocks_controller.rb +++ b/app/controllers/user_blocks_controller.rb @@ -25,11 +25,11 @@ class UserBlocksController < ApplicationController @show_user_name = true @show_creator_name = true - render :partial => "blocks" if turbo_frame_request_id == "pagination" + render :partial => "page" if turbo_frame_request_id == "pagination" end def show - if current_user && current_user == @user_block.user + if current_user && current_user == @user_block.user && !@user_block.deactivates_at @user_block.needs_view = false @user_block.deactivates_at = [@user_block.ends_at, Time.now.utc].max @user_block.save! @@ -71,7 +71,7 @@ class UserBlocksController < ApplicationController def update if @valid_params if cannot?(:update, @user_block) - flash[:error] = t(@user_block.revoker ? ".only_creator_or_revoker_can_edit" : ".only_creator_can_edit") + flash[:error] = @user_block.revoker ? t(".only_creator_or_revoker_can_edit") : t(".only_creator_can_edit") redirect_to :action => "edit" else user_block_was_active = @user_block.active? @@ -127,7 +127,7 @@ class UserBlocksController < ApplicationController @show_user_name = false @show_creator_name = true - render :partial => "blocks" if turbo_frame_request_id == "pagination" + render :partial => "page" if turbo_frame_request_id == "pagination" end ## @@ -142,7 +142,7 @@ class UserBlocksController < ApplicationController @show_user_name = true @show_creator_name = false - render :partial => "blocks" if turbo_frame_request_id == "pagination" + render :partial => "page" if turbo_frame_request_id == "pagination" end private diff --git a/app/controllers/user_roles_controller.rb b/app/controllers/user_roles_controller.rb index 469b2c40b..912453be8 100644 --- a/app/controllers/user_roles_controller.rb +++ b/app/controllers/user_roles_controller.rb @@ -9,15 +9,15 @@ class UserRolesController < ApplicationController before_action :lookup_user before_action :require_valid_role - before_action :not_in_role, :only => [:grant] - before_action :in_role, :only => [:revoke] + before_action :not_in_role, :only => :create + before_action :in_role, :only => :destroy - def grant + def create @user.roles.create(:role => @role, :granter => current_user) redirect_to user_path(@user) end - def revoke + def destroy # checks that administrator role is not revoked from current user if current_user == @user && @role == "administrator" flash[:error] = t("user_role.filter.not_revoke_admin_current_user") diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4ebeb1ec3..63a83ad1d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -36,9 +36,11 @@ class UsersController < ApplicationController users = User.all users = users.where(:status => @params[:status]) if @params[:status] - users = users.where(:creation_ip => @params[:ip]) if @params[:ip] + users = users.where(:creation_address => @params[:ip]) if @params[:ip] + + @users_count = users.limit(501).count + @users_count = I18n.t("count.at_least_pattern", :count => 500) if @users_count > 500 - @users_count = users.count @users, @newer_users_id, @older_users_id = get_page_items(users, :limit => 50) render :partial => "page" if turbo_frame_request_id == "pagination" @@ -266,7 +268,7 @@ class UsersController < ApplicationController def save_new_user(email_hmac, referer = nil) current_user.data_public = true current_user.description = "" if current_user.description.nil? - current_user.creation_ip = request.remote_ip + current_user.creation_address = request.remote_ip current_user.languages = http_accept_language.user_preferred_languages current_user.terms_agreed = Time.now.utc current_user.tou_agreed = Time.now.utc diff --git a/app/helpers/browse_helper.rb b/app/helpers/browse_helper.rb index c8fc8245c..69a8f8fa2 100644 --- a/app/helpers/browse_helper.rb +++ b/app/helpers/browse_helper.rb @@ -44,9 +44,9 @@ module BrowseHelper t "printable_name.version", :version => object.version end - def element_strikethrough(object, &block) + def element_strikethrough(object, &) if object.redacted? || !object.visible? - tag.s(&block) + tag.s(&) else yield end @@ -70,14 +70,14 @@ module BrowseHelper "nofollow" if object.tags.empty? end - def type_and_paginated_count(type, pages) + def type_and_paginated_count(type, pages, selected_page = pages.current_page) if pages.page_count == 1 t ".#{type.pluralize}", :count => pages.item_count else t ".#{type.pluralize}_paginated", - :x => pages.current_page.first_item, - :y => pages.current_page.last_item, + :x => selected_page.first_item, + :y => selected_page.last_item, :count => pages.item_count end end @@ -86,21 +86,21 @@ module BrowseHelper max_width_for_default_padding = 35 width = 0 - pagination_items(pages, {}).each do |body| + pagination_items(pages, {}).each do |(body)| width += 2 # padding width width += body.length end link_classes = ["page-link", { "px-1" => width > max_width_for_default_padding }] - tag.ul :class => "pagination pagination-sm mb-1 ms-auto" do - pagination_items(pages, {}).each do |body, n| - linked = !(n.is_a? String) + tag.ul :class => "pagination pagination-sm mb-2" do + pagination_items(pages, {}).each do |body, page_or_class| + linked = !(page_or_class.is_a? String) link = if linked - link_to body, url_for(page_param => n), :class => link_classes + link_to body, url_for(page_param => page_or_class.number), :class => link_classes, **yield(page_or_class) else tag.span body, :class => link_classes end - concat tag.li link, :class => ["page-item", { n => !linked }] + concat tag.li link, :class => ["page-item", { page_or_class => !linked }] end end end diff --git a/app/helpers/open_graph_helper.rb b/app/helpers/open_graph_helper.rb index cde848e5d..a496268e4 100644 --- a/app/helpers/open_graph_helper.rb +++ b/app/helpers/open_graph_helper.rb @@ -4,7 +4,7 @@ module OpenGraphHelper def opengraph_tags(title, properties) tags = { "og:site_name" => t("layouts.project_name.title"), - "og:title" => title || t("layouts.project_name.title"), + "og:title" => properties["og:title"] || title || t("layouts.project_name.title"), "og:type" => "website", "og:url" => url_for(:only_path => false), "og:description" => properties["og:description"] || t("layouts.intro_text") diff --git a/app/helpers/user_helper.rb b/app/helpers/user_helper.rb index 0831cde6e..e00a2253f 100644 --- a/app/helpers/user_helper.rb +++ b/app/helpers/user_helper.rb @@ -60,7 +60,7 @@ module UserHelper :size => "36"), auth_path(options.merge(:provider => provider)), :method => :post, - :class => "auth_button btn btn-light p-2", + :class => "auth_button btn btn-outline-secondary border p-2", :title => t("application.auth_providers.#{provider}.title") ) end diff --git a/app/helpers/user_mailer_helper.rb b/app/helpers/user_mailer_helper.rb index e5d6e39ac..d47827074 100644 --- a/app/helpers/user_mailer_helper.rb +++ b/app/helpers/user_mailer_helper.rb @@ -18,10 +18,10 @@ module UserMailerHelper ) end - def message_body(&block) + def message_body(&) render( :partial => "message_body", - :locals => { :body => capture(&block) } + :locals => { :body => capture(&) } ) end diff --git a/app/helpers/user_roles_helper.rb b/app/helpers/user_roles_helper.rb index e839c0ae6..02017bdb9 100644 --- a/app/helpers/user_roles_helper.rb +++ b/app/helpers/user_roles_helper.rb @@ -7,12 +7,12 @@ module UserRolesHelper if current_user&.administrator? if user.role?(role) link_to role_icon_svg_tag(role, false, t("users.show.role.revoke.#{role}")), - revoke_role_path(user, role), - :method => :post, + user_role_path(user, role), + :method => :delete, :data => { :confirm => t("user_role.revoke.are_you_sure", :name => user.display_name, :role => role) } else link_to role_icon_svg_tag(role, true, t("users.show.role.grant.#{role}")), - grant_role_path(user, role), + user_role_path(user, role), :method => :post, :data => { :confirm => t("user_role.grant.are_you_sure", :name => user.display_name, :role => role) } end diff --git a/app/jobs/trace_importer_job.rb b/app/jobs/trace_importer_job.rb index 940dd6c79..48285cbc1 100644 --- a/app/jobs/trace_importer_job.rb +++ b/app/jobs/trace_importer_job.rb @@ -10,6 +10,10 @@ class TraceImporterJob < ApplicationJob UserMailer.gpx_failure(trace, "0 points parsed ok. Do they all have lat,lng,alt,timestamp?").deliver trace.destroy end + rescue XML::Error => e + logger.info e.to_s + UserMailer.gpx_failure(trace, e).deliver + trace.destroy rescue StandardError => e logger.info e.to_s e.backtrace.each { |l| logger.info l } diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 86e77703b..dee3dafbe 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -247,8 +247,8 @@ class UserMailer < ApplicationMailer end end - def with_recipient_locale(recipient, &block) - I18n.with_locale(Locale.available.preferred(recipient.preferred_languages), &block) + def with_recipient_locale(recipient, &) + I18n.with_locale(Locale.available.preferred(recipient.preferred_languages), &) end def from_address(name, type, id, token, user_id = nil) diff --git a/app/models/access_token.rb b/app/models/access_token.rb deleted file mode 100644 index d2c0dba1c..000000000 --- a/app/models/access_token.rb +++ /dev/null @@ -1,57 +0,0 @@ -# == Schema Information -# -# Table name: oauth_tokens -# -# id :integer not null, primary key -# user_id :integer -# type :string(20) -# client_application_id :integer -# token :string(50) -# secret :string(50) -# authorized_at :datetime -# invalidated_at :datetime -# created_at :datetime -# updated_at :datetime -# allow_read_prefs :boolean default(FALSE), not null -# allow_write_prefs :boolean default(FALSE), not null -# allow_write_diary :boolean default(FALSE), not null -# allow_write_api :boolean default(FALSE), not null -# allow_read_gpx :boolean default(FALSE), not null -# allow_write_gpx :boolean default(FALSE), not null -# callback_url :string -# verifier :string(20) -# scope :string -# valid_to :datetime -# allow_write_notes :boolean default(FALSE), not null -# -# Indexes -# -# index_oauth_tokens_on_token (token) UNIQUE -# index_oauth_tokens_on_user_id (user_id) -# -# Foreign Keys -# -# oauth_tokens_client_application_id_fkey (client_application_id => client_applications.id) -# oauth_tokens_user_id_fkey (user_id => users.id) -# - -class AccessToken < OauthToken - belongs_to :user, :optional => true - belongs_to :client_application, :optional => true - - scope :valid, -> { where(:invalidated_at => nil) } - - validates :user, :secret, :presence => true - - before_create :set_authorized_at - - def includes_scope?(scope) - self[:"allow_#{scope}"] - end - - protected - - def set_authorized_at - self.authorized_at = Time.now.utc - end -end diff --git a/app/models/client_application.rb b/app/models/client_application.rb deleted file mode 100644 index d9d6b2df8..000000000 --- a/app/models/client_application.rb +++ /dev/null @@ -1,109 +0,0 @@ -# == Schema Information -# -# Table name: client_applications -# -# id :integer not null, primary key -# name :string -# url :string -# support_url :string -# callback_url :string -# key :string(50) -# secret :string(50) -# user_id :integer -# created_at :datetime -# updated_at :datetime -# allow_read_prefs :boolean default(FALSE), not null -# allow_write_prefs :boolean default(FALSE), not null -# allow_write_diary :boolean default(FALSE), not null -# allow_write_api :boolean default(FALSE), not null -# allow_read_gpx :boolean default(FALSE), not null -# allow_write_gpx :boolean default(FALSE), not null -# allow_write_notes :boolean default(FALSE), not null -# -# Indexes -# -# index_client_applications_on_key (key) UNIQUE -# index_client_applications_on_user_id (user_id) -# -# Foreign Keys -# -# client_applications_user_id_fkey (user_id => users.id) -# - -class ClientApplication < ApplicationRecord - belongs_to :user, :optional => true - has_many :tokens, :class_name => "OauthToken", :dependent => :delete_all - has_many :access_tokens - has_many :oauth2_verifiers - has_many :oauth_tokens - - validates :key, :presence => true, :uniqueness => true - validates :name, :url, :secret, :presence => true - validates :url, :format => /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/ - validates :support_url, :allow_blank => true, :format => /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/ - validates :callback_url, :allow_blank => true, :format => /\A#{URI::DEFAULT_PARSER.make_regexp}\z/ - - before_validation :generate_keys, :on => :create - - attr_accessor :token_callback_url - - def self.find_token(token_key) - token = OauthToken.includes(:client_application).find_by(:token => token_key) - token if token&.authorized? - end - - def self.verify_request(request, options = {}, &block) - signature = OAuth::Signature.build(request, options, &block) - return false unless OauthNonce.remember(signature.request.nonce, signature.request.timestamp) - - signature.verify - rescue OAuth::Signature::UnknownSignatureMethod - false - end - - def self.all_permissions - Oauth.scopes.collect { |s| :"allow_#{s.name}" } - end - - def oauth_server - @oauth_server ||= OAuth::Server.new("https://#{Settings.server_url}") - end - - def credentials - @credentials ||= OAuth::Consumer.new(key, secret) - end - - def create_request_token(_params = {}) - params = { :client_application => self, :callback_url => token_callback_url } - permissions.each do |p| - params[p] = true - end - RequestToken.create(params) - end - - def access_token_for_user(user) - unless token = access_tokens.valid.find_by(:user_id => user) - params = { :user => user } - - permissions.each do |p| - params[p] = true - end - - token = access_tokens.create(params) - end - - token - end - - # the permissions that this client would like from the user - def permissions - ClientApplication.all_permissions.select { |p| self[p] } - end - - protected - - def generate_keys - self.key = OAuth::Helper.generate_key(40)[0, 40] - self.secret = OAuth::Helper.generate_key(40)[0, 40] - end -end diff --git a/app/models/node.rb b/app/models/node.rb index 825336d16..1bec9a33c 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -199,10 +199,6 @@ class Node < ApplicationRecord save_with_history! end - def tags_as_hash - tags - end - def tags @tags ||= node_tags.to_h { |t| [t.k, t.v] } end diff --git a/app/models/note.rb b/app/models/note.rb index 0b0597434..6d8ca078f 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -23,6 +23,8 @@ class Note < ApplicationRecord has_many :comments, -> { left_joins(:author).where(:visible => true, :users => { :status => [nil, "active", "confirmed"] }).order(:created_at) }, :class_name => "NoteComment", :foreign_key => :note_id has_many :all_comments, -> { left_joins(:author).order(:created_at) }, :class_name => "NoteComment", :foreign_key => :note_id, :inverse_of => :note + has_many :subscriptions, :class_name => "NoteSubscription" + has_many :subscribers, :through => :subscriptions, :source => :user validates :id, :uniqueness => true, :presence => { :on => :update }, :numericality => { :on => :update, :only_integer => true } diff --git a/app/models/note_subscription.rb b/app/models/note_subscription.rb new file mode 100644 index 000000000..76e8a226c --- /dev/null +++ b/app/models/note_subscription.rb @@ -0,0 +1,20 @@ +# == Schema Information +# +# Table name: note_subscriptions +# +# user_id :bigint(8) not null, primary key +# note_id :bigint(8) not null, primary key +# +# Indexes +# +# index_note_subscriptions_on_note_id (note_id) +# +# Foreign Keys +# +# fk_rails_... (note_id => notes.id) +# fk_rails_... (user_id => users.id) +# +class NoteSubscription < ApplicationRecord + belongs_to :user + belongs_to :note +end diff --git a/app/models/oauth2_token.rb b/app/models/oauth2_token.rb deleted file mode 100644 index 3435b25a3..000000000 --- a/app/models/oauth2_token.rb +++ /dev/null @@ -1,58 +0,0 @@ -# == Schema Information -# -# Table name: oauth_tokens -# -# id :integer not null, primary key -# user_id :integer -# type :string(20) -# client_application_id :integer -# token :string(50) -# secret :string(50) -# authorized_at :datetime -# invalidated_at :datetime -# created_at :datetime -# updated_at :datetime -# allow_read_prefs :boolean default(FALSE), not null -# allow_write_prefs :boolean default(FALSE), not null -# allow_write_diary :boolean default(FALSE), not null -# allow_write_api :boolean default(FALSE), not null -# allow_read_gpx :boolean default(FALSE), not null -# allow_write_gpx :boolean default(FALSE), not null -# callback_url :string -# verifier :string(20) -# scope :string -# valid_to :datetime -# allow_write_notes :boolean default(FALSE), not null -# -# Indexes -# -# index_oauth_tokens_on_token (token) UNIQUE -# index_oauth_tokens_on_user_id (user_id) -# -# Foreign Keys -# -# oauth_tokens_client_application_id_fkey (client_application_id => client_applications.id) -# oauth_tokens_user_id_fkey (user_id => users.id) -# - -class Oauth2Token < AccessToken - attr_accessor :state - - def as_json(_options = {}) - d = { :access_token => token, :token_type => "bearer" } - d[:expires_in] = expires_in if expires_at - d - end - - def to_query - q = "access_token=#{token}&token_type=bearer" - q << "&state=#{CGI.escape(state)}" if @state - q << "&expires_in=#{expires_in}" if expires_at - q << "&scope=#{CGI.escape(scope)}" if scope - q - end - - def expires_in - expires_at.to_i - Time.now.to_i - end -end diff --git a/app/models/oauth2_verifier.rb b/app/models/oauth2_verifier.rb deleted file mode 100644 index 1613eabeb..000000000 --- a/app/models/oauth2_verifier.rb +++ /dev/null @@ -1,72 +0,0 @@ -# == Schema Information -# -# Table name: oauth_tokens -# -# id :integer not null, primary key -# user_id :integer -# type :string(20) -# client_application_id :integer -# token :string(50) -# secret :string(50) -# authorized_at :datetime -# invalidated_at :datetime -# created_at :datetime -# updated_at :datetime -# allow_read_prefs :boolean default(FALSE), not null -# allow_write_prefs :boolean default(FALSE), not null -# allow_write_diary :boolean default(FALSE), not null -# allow_write_api :boolean default(FALSE), not null -# allow_read_gpx :boolean default(FALSE), not null -# allow_write_gpx :boolean default(FALSE), not null -# callback_url :string -# verifier :string(20) -# scope :string -# valid_to :datetime -# allow_write_notes :boolean default(FALSE), not null -# -# Indexes -# -# index_oauth_tokens_on_token (token) UNIQUE -# index_oauth_tokens_on_user_id (user_id) -# -# Foreign Keys -# -# oauth_tokens_client_application_id_fkey (client_application_id => client_applications.id) -# oauth_tokens_user_id_fkey (user_id => users.id) -# - -class Oauth2Verifier < OauthToken - validates :user, :presence => true, :associated => true - - attr_accessor :state - - def exchange!(_params = {}) - OauthToken.transaction do - token = Oauth2Token.create! :user => user, :client_application => client_application, :scope => scope - invalidate! - token - end - end - - def code - token - end - - def redirect_url - callback_url - end - - def to_query - q = "code=#{token}" - q << "&state=#{CGI.escape(state)}" if @state - q - end - - protected - - def generate_keys - self.token = OAuth::Helper.generate_key(20)[0, 20] - self.expires_at = 10.minutes.from_now - self.authorized_at = Time.now.utc - end -end diff --git a/app/models/oauth_nonce.rb b/app/models/oauth_nonce.rb deleted file mode 100644 index e08121cfc..000000000 --- a/app/models/oauth_nonce.rb +++ /dev/null @@ -1,31 +0,0 @@ -# == Schema Information -# -# Table name: oauth_nonces -# -# id :bigint(8) not null, primary key -# nonce :string -# timestamp :integer -# created_at :datetime -# updated_at :datetime -# -# Indexes -# -# index_oauth_nonces_on_nonce_and_timestamp (nonce,timestamp) UNIQUE -# - -# Simple store of nonces. The OAuth Spec requires that any given pair of nonce and timestamps are unique. -# Thus you can use the same nonce with a different timestamp and viceversa. -class OauthNonce < ApplicationRecord - validates :timestamp, :presence => true - validates :nonce, :presence => true, :uniqueness => { :scope => :timestamp } - - # Remembers a nonce and it's associated timestamp. It returns false if it has already been used - def self.remember(nonce, timestamp) - return false if Time.now.to_i - timestamp.to_i > 86400 - - oauth_nonce = OauthNonce.create(:nonce => nonce, :timestamp => timestamp.to_i) - return false if oauth_nonce.new_record? - - oauth_nonce - end -end diff --git a/app/models/oauth_token.rb b/app/models/oauth_token.rb deleted file mode 100644 index ae15dc658..000000000 --- a/app/models/oauth_token.rb +++ /dev/null @@ -1,72 +0,0 @@ -# == Schema Information -# -# Table name: oauth_tokens -# -# id :integer not null, primary key -# user_id :integer -# type :string(20) -# client_application_id :integer -# token :string(50) -# secret :string(50) -# authorized_at :datetime -# invalidated_at :datetime -# created_at :datetime -# updated_at :datetime -# allow_read_prefs :boolean default(FALSE), not null -# allow_write_prefs :boolean default(FALSE), not null -# allow_write_diary :boolean default(FALSE), not null -# allow_write_api :boolean default(FALSE), not null -# allow_read_gpx :boolean default(FALSE), not null -# allow_write_gpx :boolean default(FALSE), not null -# callback_url :string -# verifier :string(20) -# scope :string -# valid_to :datetime -# allow_write_notes :boolean default(FALSE), not null -# -# Indexes -# -# index_oauth_tokens_on_token (token) UNIQUE -# index_oauth_tokens_on_user_id (user_id) -# -# Foreign Keys -# -# oauth_tokens_client_application_id_fkey (client_application_id => client_applications.id) -# oauth_tokens_user_id_fkey (user_id => users.id) -# - -class OauthToken < ApplicationRecord - belongs_to :client_application, :optional => true - belongs_to :user, :optional => true - - scope :authorized, -> { where("authorized_at IS NOT NULL and invalidated_at IS NULL") } - - validates :token, :presence => true, :uniqueness => true - validates :user, :associated => true - validates :client_application, :presence => true - - before_validation :generate_keys, :on => :create - - def invalidated? - invalidated_at != nil - end - - def invalidate! - update(:invalidated_at => Time.now.utc) - end - - def authorized? - !authorized_at.nil? && !invalidated? - end - - def to_query - "oauth_token=#{token}&oauth_token_secret=#{secret}" - end - - protected - - def generate_keys - self.token = OAuth::Helper.generate_key(40)[0, 40] - self.secret = OAuth::Helper.generate_key(40)[0, 40] - end -end diff --git a/app/models/old_node.rb b/app/models/old_node.rb index b323f0b4f..f29eed9dd 100644 --- a/app/models/old_node.rb +++ b/app/models/old_node.rb @@ -47,7 +47,7 @@ class OldNode < ApplicationRecord belongs_to :redaction, :optional => true belongs_to :current_node, :class_name => "Node", :foreign_key => "node_id", :inverse_of => :old_nodes - has_many :old_tags, :class_name => "OldNodeTag", :query_constraints => [:node_id, :version], :inverse_of => :old_node + has_many :old_tags, :class_name => "OldNodeTag", :foreign_key => [:node_id, :version], :inverse_of => :old_node def validate_position errors.add(:base, "Node is not in the world") unless in_world? @@ -85,10 +85,6 @@ class OldNode < ApplicationRecord attr_writer :tags - def tags_as_hash - tags - end - # Pretend we're not in any ways def ways [] diff --git a/app/models/old_node_tag.rb b/app/models/old_node_tag.rb index 8f632a671..503ecf27a 100644 --- a/app/models/old_node_tag.rb +++ b/app/models/old_node_tag.rb @@ -15,7 +15,7 @@ class OldNodeTag < ApplicationRecord self.table_name = "node_tags" - belongs_to :old_node, :query_constraints => [:node_id, :version], :inverse_of => :old_tags + belongs_to :old_node, :foreign_key => [:node_id, :version], :inverse_of => :old_tags validates :old_node, :associated => true validates :k, :v, :allow_blank => true, :length => { :maximum => 255 }, :characters => true diff --git a/app/models/old_relation.rb b/app/models/old_relation.rb index 7d5a3fbb4..9f551d839 100644 --- a/app/models/old_relation.rb +++ b/app/models/old_relation.rb @@ -31,8 +31,8 @@ class OldRelation < ApplicationRecord belongs_to :redaction, :optional => true belongs_to :current_relation, :class_name => "Relation", :foreign_key => "relation_id", :inverse_of => :old_relations - has_many :old_members, -> { order(:sequence_id) }, :class_name => "OldRelationMember", :query_constraints => [:relation_id, :version], :inverse_of => :old_relation - has_many :old_tags, :class_name => "OldRelationTag", :query_constraints => [:relation_id, :version], :inverse_of => :old_relation + has_many :old_members, -> { order(:sequence_id) }, :class_name => "OldRelationMember", :foreign_key => [:relation_id, :version], :inverse_of => :old_relation + has_many :old_tags, :class_name => "OldRelationTag", :foreign_key => [:relation_id, :version], :inverse_of => :old_relation validates :changeset, :associated => true validates :timestamp, :presence => true diff --git a/app/models/old_relation_member.rb b/app/models/old_relation_member.rb index 5a1156605..6d5aaf5c9 100644 --- a/app/models/old_relation_member.rb +++ b/app/models/old_relation_member.rb @@ -21,7 +21,7 @@ class OldRelationMember < ApplicationRecord self.table_name = "relation_members" - belongs_to :old_relation, :query_constraints => [:relation_id, :version], :inverse_of => :old_members + belongs_to :old_relation, :foreign_key => [:relation_id, :version], :inverse_of => :old_members # A bit messy, referring to the current tables, should do for the data browser for now belongs_to :member, :polymorphic => true diff --git a/app/models/old_relation_tag.rb b/app/models/old_relation_tag.rb index 31399c00d..39566aeb9 100644 --- a/app/models/old_relation_tag.rb +++ b/app/models/old_relation_tag.rb @@ -15,7 +15,7 @@ class OldRelationTag < ApplicationRecord self.table_name = "relation_tags" - belongs_to :old_relation, :query_constraints => [:relation_id, :version], :inverse_of => :old_tags + belongs_to :old_relation, :foreign_key => [:relation_id, :version], :inverse_of => :old_tags validates :old_relation, :associated => true validates :k, :v, :allow_blank => true, :length => { :maximum => 255 }, :characters => true diff --git a/app/models/old_way.rb b/app/models/old_way.rb index 8577330c5..0c53f90bd 100644 --- a/app/models/old_way.rb +++ b/app/models/old_way.rb @@ -31,8 +31,8 @@ class OldWay < ApplicationRecord belongs_to :redaction, :optional => true belongs_to :current_way, :class_name => "Way", :foreign_key => "way_id", :inverse_of => :old_ways - has_many :old_nodes, :class_name => "OldWayNode", :query_constraints => [:way_id, :version], :inverse_of => :old_way - has_many :old_tags, :class_name => "OldWayTag", :query_constraints => [:way_id, :version], :inverse_of => :old_way + has_many :old_nodes, :class_name => "OldWayNode", :foreign_key => [:way_id, :version], :inverse_of => :old_way + has_many :old_tags, :class_name => "OldWayTag", :foreign_key => [:way_id, :version], :inverse_of => :old_way validates :changeset, :associated => true validates :timestamp, :presence => true diff --git a/app/models/old_way_node.rb b/app/models/old_way_node.rb index e031aba10..b2a16c16b 100644 --- a/app/models/old_way_node.rb +++ b/app/models/old_way_node.rb @@ -19,7 +19,7 @@ class OldWayNode < ApplicationRecord self.table_name = "way_nodes" - belongs_to :old_way, :query_constraints => [:way_id, :version], :inverse_of => :old_nodes + belongs_to :old_way, :foreign_key => [:way_id, :version], :inverse_of => :old_nodes # A bit messy, referring to current nodes and ways, should do for the data browser for now belongs_to :node belongs_to :way diff --git a/app/models/old_way_tag.rb b/app/models/old_way_tag.rb index 96ec8baf5..82ce132ec 100644 --- a/app/models/old_way_tag.rb +++ b/app/models/old_way_tag.rb @@ -15,7 +15,7 @@ class OldWayTag < ApplicationRecord self.table_name = "way_tags" - belongs_to :old_way, :query_constraints => [:way_id, :version], :inverse_of => :old_tags + belongs_to :old_way, :foreign_key => [:way_id, :version], :inverse_of => :old_tags validates :old_way, :associated => true validates :k, :v, :allow_blank => true, :length => { :maximum => 255 }, :characters => true diff --git a/app/models/relation.rb b/app/models/relation.rb index f09647320..69e2acfd1 100644 --- a/app/models/relation.rb +++ b/app/models/relation.rb @@ -234,7 +234,7 @@ class Relation < ApplicationRecord element = model.lock("for share").find_by(:id => m[1]) # and check that it is OK to use. - raise OSM::APIPreconditionFailedError, "Relation with id #{id} cannot be saved due to #{m[0]} with id #{m[1]}" unless element&.visible? && element&.preconditions_ok? + raise OSM::APIPreconditionFailedError, "Relation with id #{id} cannot be saved due to #{m[0]} with id #{m[1]}" unless element&.visible? && element.preconditions_ok? hash[m[1]] = true end diff --git a/app/models/request_token.rb b/app/models/request_token.rb deleted file mode 100644 index ec38f7421..000000000 --- a/app/models/request_token.rb +++ /dev/null @@ -1,82 +0,0 @@ -# == Schema Information -# -# Table name: oauth_tokens -# -# id :integer not null, primary key -# user_id :integer -# type :string(20) -# client_application_id :integer -# token :string(50) -# secret :string(50) -# authorized_at :datetime -# invalidated_at :datetime -# created_at :datetime -# updated_at :datetime -# allow_read_prefs :boolean default(FALSE), not null -# allow_write_prefs :boolean default(FALSE), not null -# allow_write_diary :boolean default(FALSE), not null -# allow_write_api :boolean default(FALSE), not null -# allow_read_gpx :boolean default(FALSE), not null -# allow_write_gpx :boolean default(FALSE), not null -# callback_url :string -# verifier :string(20) -# scope :string -# valid_to :datetime -# allow_write_notes :boolean default(FALSE), not null -# -# Indexes -# -# index_oauth_tokens_on_token (token) UNIQUE -# index_oauth_tokens_on_user_id (user_id) -# -# Foreign Keys -# -# oauth_tokens_client_application_id_fkey (client_application_id => client_applications.id) -# oauth_tokens_user_id_fkey (user_id => users.id) -# - -class RequestToken < OauthToken - attr_accessor :provided_oauth_verifier - - def authorize!(user) - return false if authorized? - - self.user = user - self.authorized_at = Time.now.utc - self.verifier = OAuth::Helper.generate_key(20)[0, 20] unless oauth10? - save - end - - def exchange! - return false unless authorized? - return false unless oauth10? || verifier == provided_oauth_verifier - - RequestToken.transaction do - params = { :user => user, :client_application => client_application } - # copy the permissions from the authorised request token to the access token - client_application.permissions.each do |p| - params[p] = self[p] - end - - access_token = AccessToken.create(params) - invalidate! - access_token - end - end - - def to_query - if oauth10? - super - else - "#{super}&oauth_callback_confirmed=true" - end - end - - def oob? - callback_url.nil? || callback_url.casecmp?("oob") - end - - def oauth10? - Settings.key?(:oauth_10_support) && Settings.oauth_10_support && callback_url.blank? - end -end diff --git a/app/models/user.rb b/app/models/user.rb index 4241a9e56..917faca21 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,7 +15,6 @@ # pass_salt :string # email_valid :boolean default(FALSE), not null # new_email :string -# creation_ip :string # languages :string # status :enum default("pending"), not null # terms_agreed :datetime @@ -33,9 +32,11 @@ # tou_agreed :datetime # diary_comments_count :integer default(0) # note_comments_count :integer default(0) +# creation_address :inet # # Indexes # +# index_users_on_creation_address (creation_address) USING gist # users_auth_idx (auth_provider,auth_uid) UNIQUE # users_display_name_canonical_idx (lower(NORMALIZE(display_name, NFKC))) # users_display_name_idx (display_name) UNIQUE @@ -65,9 +66,8 @@ class User < ApplicationRecord has_and_belongs_to_many :changeset_subscriptions, :class_name => "Changeset", :join_table => "changesets_subscribers", :foreign_key => "subscriber_id" has_many :note_comments, :foreign_key => :author_id, :inverse_of => :author has_many :notes, :through => :note_comments - - has_many :client_applications - has_many :oauth_tokens, -> { order(:authorized_at => :desc).preload(:client_application) }, :class_name => "OauthToken", :inverse_of => :user + has_many :note_subscriptions, :class_name => "NoteSubscription" + has_many :subscribed_notes, :through => :note_subscriptions, :source => :note has_many :oauth2_applications, :class_name => Doorkeeper.config.application_model.name, :as => :owner has_many :access_grants, :class_name => Doorkeeper.config.access_grant_model.name, :foreign_key => :resource_owner_id @@ -332,7 +332,6 @@ class User < ApplicationRecord ## # revoke any authentication tokens def revoke_authentication_tokens - oauth_tokens.authorized.each(&:invalidate!) access_tokens.not_expired.each(&:revoke) end @@ -377,12 +376,6 @@ class User < ApplicationRecord suspend! if may_suspend? && spam_score > Settings.spam_threshold end - ## - # return an oauth 1 access token for a specified application - def access_token(application_key) - ClientApplication.find_by(:key => application_key).access_token_for_user(self) - end - ## # return an oauth 2 access token for a specified application def oauth_token(application_id) @@ -430,8 +423,8 @@ class User < ApplicationRecord if moderator? Settings.moderator_changeset_comments_per_hour else - previous_comments = changeset_comments.limit(200).count - max_comments = previous_comments / 200.0 * Settings.max_changeset_comments_per_hour + previous_comments = changeset_comments.limit(Settings.comments_to_max_changeset_comments).count + max_comments = previous_comments / Settings.comments_to_max_changeset_comments.to_f * Settings.max_changeset_comments_per_hour max_comments = max_comments.floor.clamp(Settings.initial_changeset_comments_per_hour, Settings.max_changeset_comments_per_hour) max_comments /= 2**active_reports max_comments.floor.clamp(Settings.min_changeset_comments_per_hour, Settings.max_changeset_comments_per_hour) diff --git a/app/views/application/_auth_providers.html.erb b/app/views/application/_auth_providers.html.erb index f6665cefe..3feda6139 100644 --- a/app/views/application/_auth_providers.html.erb +++ b/app/views/application/_auth_providers.html.erb @@ -20,7 +20,7 @@ :data => { "bs-toggle" => "collapse", "bs-target" => "#login_auth_buttons, #openid_login_form" }, :title => t(".openid.title"), - :class => "btn btn-light p-2" %> + :class => "btn btn-outline-secondary border p-2" %> <% elsif provider != @preferred_auth_provider %> <%= auth_button provider %> <% end -%> diff --git a/app/views/application/_settings_menu.html.erb b/app/views/application/_settings_menu.html.erb index 8477a11a0..14f1c2927 100644 --- a/app/views/application/_settings_menu.html.erb +++ b/app/views/application/_settings_menu.html.erb @@ -5,9 +5,6 @@ - diff --git a/app/views/changeset_comments/_comment.html.erb b/app/views/changeset_comments/feeds/_comment.html.erb similarity index 100% rename from app/views/changeset_comments/_comment.html.erb rename to app/views/changeset_comments/feeds/_comment.html.erb diff --git a/app/views/changeset_comments/_comment.rss.builder b/app/views/changeset_comments/feeds/_comment.rss.builder similarity index 100% rename from app/views/changeset_comments/_comment.rss.builder rename to app/views/changeset_comments/feeds/_comment.rss.builder diff --git a/app/views/changeset_comments/index.rss.builder b/app/views/changeset_comments/feeds/show.rss.builder similarity index 79% rename from app/views/changeset_comments/index.rss.builder rename to app/views/changeset_comments/feeds/show.rss.builder index f055c2014..c86b938f7 100644 --- a/app/views/changeset_comments/index.rss.builder +++ b/app/views/changeset_comments/feeds/show.rss.builder @@ -6,7 +6,7 @@ xml.rss("version" => "2.0", else xml.title t(".title_all") end - xml.link url_for(:controller => "site", :action => "index", :only_path => false) + xml.link root_url xml << render(:partial => "comment", :collection => @comments) end diff --git a/app/views/changeset_comments/feeds/timeout.rss.builder b/app/views/changeset_comments/feeds/timeout.rss.builder new file mode 100644 index 000000000..c56ebb879 --- /dev/null +++ b/app/views/changeset_comments/feeds/timeout.rss.builder @@ -0,0 +1,12 @@ +xml.rss("version" => "2.0", + "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do + xml.channel do + if params[:changeset_id] + xml.title t("changeset_comments.feeds.show.title_particular", :changeset_id => params[:changeset_id]) + else + xml.title t("changeset_comments.feeds.show.title_all") + end + xml.link root_url + xml.description t(".sorry") + end +end diff --git a/app/views/changeset_comments/timeout.atom.builder b/app/views/changeset_comments/timeout.atom.builder deleted file mode 100644 index b5eeeed4a..000000000 --- a/app/views/changeset_comments/timeout.atom.builder +++ /dev/null @@ -1,12 +0,0 @@ -atom_feed(:language => I18n.locale, :schema_date => 2009, - :id => url_for(params.merge(:only_path => false)), - :root_url => url_for(params.merge(:only_path => false, :format => nil)), - "xmlns:georss" => "http://www.georss.org/georss") do |feed| - feed.title @title - - feed.subtitle :type => "xhtml" do |xhtml| - xhtml.p do |p| - p << t(".sorry") - end - end -end diff --git a/app/views/changeset_comments/timeout.html.erb b/app/views/changeset_comments/timeout.html.erb deleted file mode 100644 index 641b1dfb8..000000000 --- a/app/views/changeset_comments/timeout.html.erb +++ /dev/null @@ -1 +0,0 @@ -

<%= t ".sorry" %>

diff --git a/app/views/changesets/_elements.html.erb b/app/views/changesets/_elements.html.erb new file mode 100644 index 000000000..fd5dd8a26 --- /dev/null +++ b/app/views/changesets/_elements.html.erb @@ -0,0 +1,12 @@ +<%= turbo_frame_tag "changeset_#{type.pluralize}" do %> + <%= render :partial => "paging_nav", :locals => { :type => type, :pages => pages } %> +
    + <% elements.each do |element| %> + <%= element_list_item type, element do + t "printable_name.current_and_old_links_html", + :current_link => link_to(printable_element_name(element), :controller => type.pluralize, :action => :show, :id => element.id[0]), + :old_link => link_to(printable_element_version(element), :controller => "old_#{type.pluralize}", :action => :show, :id => element.id[0], :version => element.version) + end %> + <% end %> +
+<% end %> diff --git a/app/views/changesets/_paging_nav.html.erb b/app/views/changesets/_paging_nav.html.erb index aa7ee23b9..058738222 100644 --- a/app/views/changesets/_paging_nav.html.erb +++ b/app/views/changesets/_paging_nav.html.erb @@ -1,6 +1,9 @@ -
-

<%= type_and_paginated_count(type, pages) %>

- <% if pages.page_count > 1 %> - <%= sidebar_classic_pagination(pages, "#{type}_page") %> - <% end %> -
+

<%= type_and_paginated_count(type, pages) %>

+<% if pages.page_count > 1 %> + <%= sidebar_classic_pagination(pages, "#{type}_page") do |page| + { + :title => type_and_paginated_count(type, pages, page), + :data => { :turbo => "true" } + } + end %> +<% end %> diff --git a/app/views/changesets/index.html.erb b/app/views/changesets/index.html.erb index 97e6351a0..fc5f7cbbc 100644 --- a/app/views/changesets/index.html.erb +++ b/app/views/changesets/index.html.erb @@ -13,9 +13,9 @@
<% end -%> <% elsif params[:bbox] %> -

<%= t(params[:max_id] ? ".no_more_area" : ".empty_area") %>

+

<%= params[:max_id] ? t(".no_more_area") : t(".empty_area") %>

<% elsif params[:display_name] %> -

<%= t(params[:max_id] ? ".no_more_user" : ".empty_user") %>

+

<%= params[:max_id] ? t(".no_more_user") : t(".empty_user") %>

<% else %> -

<%= t(params[:max_id] ? ".no_more" : ".empty") %>

+

<%= params[:max_id] ? t(".no_more") : t(".empty") %>

<% end %> diff --git a/app/views/changesets/show.html.erb b/app/views/changesets/show.html.erb index 915b0ef35..a47049e99 100644 --- a/app/views/changesets/show.html.erb +++ b/app/views/changesets/show.html.erb @@ -40,9 +40,9 @@ <% next unless comment.visible || current_user&.moderator? %>
  • - <%= t comment.visible ? ".comment_by_html" : ".hidden_comment_by_html", - :time_ago => friendly_date_ago(comment.created_at), - :user => link_to(comment.author.display_name, comment.author) %> + <%= comment_by_options = { :time_ago => friendly_date_ago(comment.created_at), + :user => link_to(comment.author.display_name, comment.author) } + comment.visible ? t(".comment_by_html", **comment_by_options) : t(".hidden_comment_by_html", **comment_by_options) %> <% if current_user&.moderator? %> — <%= tag.button t(".#{comment.visible ? 'hide' : 'unhide'}_comment"), @@ -90,42 +90,15 @@ <% end %> <% unless @ways.empty? %> - <%= render :partial => "paging_nav", :locals => { :type => "way", :pages => @way_pages } %> -
      - <% @ways.each do |way| %> - <%= element_list_item "way", way do - t "printable_name.current_and_old_links_html", - :current_link => link_to(printable_element_name(way), way_path(way.way_id)), - :old_link => link_to(printable_element_version(way), old_way_path(way.way_id, way.version)) - end %> - <% end %> -
    + <%= render :partial => "elements", :locals => { :type => "way", :elements => @ways, :pages => @way_pages } %> <% end %> <% unless @relations.empty? %> - <%= render :partial => "paging_nav", :locals => { :type => "relation", :pages => @relation_pages } %> -
      - <% @relations.each do |relation| %> - <%= element_list_item "relation", relation do - t "printable_name.current_and_old_links_html", - :current_link => link_to(printable_element_name(relation), relation_path(relation.relation_id)), - :old_link => link_to(printable_element_version(relation), old_relation_path(relation.relation_id, relation.version)) - end %> - <% end %> -
    + <%= render :partial => "elements", :locals => { :type => "relation", :elements => @relations, :pages => @relation_pages } %> <% end %> <% unless @nodes.empty? %> - <%= render :partial => "paging_nav", :locals => { :type => "node", :pages => @node_pages } %> -
      - <% @nodes.each do |node| %> - <%= element_list_item "node", node do - t "printable_name.current_and_old_links_html", - :current_link => link_to(printable_element_name(node), node_path(node.node_id), { :rel => link_follow(node) }), - :old_link => link_to(printable_element_version(node), old_node_path(node.node_id, node.version), { :rel => link_follow(node) }) - end %> - <% end %> -
    + <%= render :partial => "elements", :locals => { :type => "node", :elements => @nodes, :pages => @node_pages } %> <% end %>
  • diff --git a/app/views/diary_comments/_page.html.erb b/app/views/diary_comments/_page.html.erb index 66e40cd80..cc6019cff 100644 --- a/app/views/diary_comments/_page.html.erb +++ b/app/views/diary_comments/_page.html.erb @@ -19,8 +19,6 @@ <%= render "shared/pagination", - :newer_key => "diary_comments.page.newer_comments", - :older_key => "diary_comments.page.older_comments", :newer_id => @newer_comments_id, :older_id => @older_comments_id %> diff --git a/app/views/diary_entries/_diary_comment.html.erb b/app/views/diary_entries/_diary_comment.html.erb index dbf8a439e..b3e406816 100644 --- a/app/views/diary_entries/_diary_comment.html.erb +++ b/app/views/diary_entries/_diary_comment.html.erb @@ -13,9 +13,9 @@ <% if can? :hide, DiaryComment %> <% if diary_comment.visible? %> - <%= link_to t(".hide_link"), hide_diary_comment_path(diary_comment.diary_entry.user, diary_comment.diary_entry, diary_comment), :method => :post, :data => { :confirm => t(".confirm") } %> + <%= link_to t(".hide_link"), hide_diary_comment_path(diary_comment), :method => :post, :data => { :confirm => t(".confirm") } %> <% else %> - <%= link_to t(".unhide_link"), unhide_diary_comment_path(diary_comment.diary_entry.user, diary_comment.diary_entry, diary_comment), :method => :post, :data => { :confirm => t(".confirm") } %> + <%= link_to t(".unhide_link"), unhide_diary_comment_path(diary_comment), :method => :post, :data => { :confirm => t(".confirm") } %> <% end %> <% end %> diff --git a/app/views/diary_entries/_page.html.erb b/app/views/diary_entries/_page.html.erb index f07db9b6c..c44a5417e 100644 --- a/app/views/diary_entries/_page.html.erb +++ b/app/views/diary_entries/_page.html.erb @@ -4,8 +4,6 @@ <%= render @entries %> <%= render "shared/pagination", - :newer_key => "diary_entries.page.newer_entries", - :older_key => "diary_entries.page.older_entries", :newer_id => @newer_entries_id, :older_id => @older_entries_id %> diff --git a/app/views/geocoder/search.html.erb b/app/views/geocoder/search.html.erb index f87a4909d..56f468f2f 100644 --- a/app/views/geocoder/search.html.erb +++ b/app/views/geocoder/search.html.erb @@ -4,7 +4,7 @@ <% @sources.each do |source| %>

    - <%= t(".title.results_from_html", :results_link => link_to(t(".title.#{source[:name]}"), source[:url].to_s)) %> + <%= link_to t(".title.#{source[:name]}"), source[:url].to_s %>

    ">
    diff --git a/app/views/issues/_page.html.erb b/app/views/issues/_page.html.erb new file mode 100644 index 000000000..f625f0e04 --- /dev/null +++ b/app/views/issues/_page.html.erb @@ -0,0 +1,42 @@ + + <% if @issues.length == 0 %> + <% if params[:search_by_user].present? && !@find_user %> +

    <%= t ".user_not_found" %>

    + <% else %> +

    <%= t ".issues_not_found" %>

    + <% end %> + <% else %> + + + + + + + + + + + + <% @issues.each do |issue| %> + + + + + + + + <% end %> + +
    <%= t ".status" %><%= t ".reports" %><%= t ".reported_item" %><%= t ".reported_user" %><%= t ".last_updated" %>
    <%= t ".states.#{issue.status}" %><%= link_to t(".reports_count", :count => issue.reports_count), issue %><%= link_to reportable_title(issue.reportable), reportable_url(issue.reportable) %><%= link_to issue.reported_user.display_name, issue.reported_user if issue.reported_user %> + <% if issue.user_updated %> + <%= t ".last_updated_time_ago_user_html", :user => link_to(issue.user_updated.display_name, issue.user_updated), + :time_ago => friendly_date_ago(issue.updated_at) %> + <% else %> + <%= friendly_date_ago(issue.updated_at) %> + <% end %> +
    + <%= render "shared/pagination", + :newer_id => @newer_issues_id, + :older_id => @older_issues_id %> + <% end %> +
    diff --git a/app/views/issues/index.html.erb b/app/views/issues/index.html.erb index 523f90846..95dfbf6f2 100644 --- a/app/views/issues/index.html.erb +++ b/app/views/issues/index.html.erb @@ -4,7 +4,7 @@

    <%= t ".search_guidance" %>

    -<%= form_tag(issues_path, :method => :get) do %> +<%= form_tag(issues_path, :method => :get, :data => { "turbo" => true, "turbo-frame" => "pagination", "turbo-action" => "advance" }) do %>
    <%= select_tag :status, @@ -40,36 +40,4 @@
    <% end %> -<% if @issues.length == 0 %> -

    <%= t ".issues_not_found" %>

    -<% else %> - - - - - - - - - - - - <% @issues.each do |issue| %> - - - - - - - - <% end %> - -
    <%= t ".status" %><%= t ".reports" %><%= t ".reported_item" %><%= t ".reported_user" %><%= t ".last_updated" %>
    <%= t ".states.#{issue.status}" %><%= link_to t(".reports_count", :count => issue.reports_count), issue %><%= link_to reportable_title(issue.reportable), reportable_url(issue.reportable) %><%= link_to issue.reported_user.display_name, issue.reported_user if issue.reported_user %> - <% if issue.user_updated %> - <%= t ".last_updated_time_ago_user_html", :user => link_to(issue.user_updated.display_name, issue.user_updated), - :time_ago => friendly_date_ago(issue.updated_at) %> - <% else %> - <%= friendly_date_ago(issue.updated_at) %> - <% end %> -
    -<% end %> +<%= render :partial => "page" %> diff --git a/app/views/layouts/_banner.html.erb b/app/views/layouts/_banner.html.erb index 344c5ed71..3d2c7f774 100644 --- a/app/views/layouts/_banner.html.erb +++ b/app/views/layouts/_banner.html.erb @@ -1,5 +1,5 @@ <% unless (banner = next_banner()).nil? %> - <%= tag.div :id => "banner", :class => "position-relative", :data => { :bs_theme => token_list(:dark => banner[:dark]) } do %> + <%= tag.div :id => "banner", :class => "position-relative", :data => { :bs_theme => (banner[:dark] ? "dark" : "light") } do %> <%= link_to (image_tag banner[:img], :srcset => banner[:srcset], :alt => banner[:alt], :title => banner[:alt]), banner[:link] %>
    - -
    - <%= link_to t("layouts.history"), history_path, :class => "btn btn-outline-primary geolink flex-grow-1", :id => "history_tab" %> - <%= link_to t("layouts.export"), export_path, :class => "btn btn-outline-primary geolink", :id => "export_tab" %> +
    + <%= link_to t("layouts.edit"), + edit_path, + :class => "btn btn-outline-primary geolink editlink", + :id => "editanchor", + :data => { :editor => preferred_editor } %> + +