From 93b06b868e8825455df63eec5b8189831ca3676a Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Thu, 30 Jul 2026 21:34:56 +0200 Subject: [PATCH 01/16] Add CI with tests and Brakeman, remove Webpacker, fix privilege and timezone bugs - New CI workflow runs the test suite against MariaDB plus a Brakeman scan; the container image push now requires both to pass - Remove the unused Webpacker setup and empty scaffold controllers, helpers, and tests - Scope User#max_privilege_level to the user (previously it returned the highest access level held by anyone, letting any signed-in user pass the redactor upload check) - Fix the broken privileges.maps settings lookup in RedactorPolicy - Stop Event#to_ics and #to_fullcalendar from mutating the global time zone - Enforce CSRF with exceptions; IOF result uploads keep an explicit skip since external tools authenticate over HTTP Basic - Add real model/policy tests and fixtures (two clubs, all privilege tiers) - Add user.merge privilege level to settings --- .browserslistrc | 1 - .github/workflows/ci.yml | 115 + .github/workflows/docker-publish.yml | 51 - .ruby-version | 1 + CLAUDE.md | 2 +- Dockerfile | 5 - Gemfile | 7 +- Gemfile.lock | 15 +- app/controllers/application_controller.rb | 2 +- app/controllers/content_blocks_controller.rb | 2 - app/controllers/courses_controller.rb | 2 - app/controllers/groups_controller.rb | 2 - app/controllers/map_standards_controller.rb | 2 - app/controllers/memberships_controller.rb | 2 - app/controllers/organizers_controller.rb | 2 - app/controllers/privileges_controller.rb | 2 - app/controllers/results_controller.rb | 3 + app/controllers/roles_controller.rb | 2 - app/controllers/series_controller.rb | 2 - app/helpers/content_blocks_helper.rb | 2 - app/helpers/courses_helper.rb | 2 - app/helpers/events_helper.rb | 2 - app/helpers/groups_helper.rb | 2 - app/helpers/map_standards_helper.rb | 2 - app/helpers/maps_helper.rb | 2 - app/helpers/memberships_helper.rb | 2 - app/helpers/organizers_helper.rb | 2 - app/helpers/pages_helper.rb | 2 - app/helpers/privileges_helper.rb | 2 - app/helpers/results_helper.rb | 2 - app/helpers/roles_helper.rb | 2 - app/helpers/series_helper.rb | 2 - app/helpers/tokens_helper.rb | 2 - app/javascript/packs/application.js | 18 - app/models/event.rb | 6 +- app/models/user.rb | 1 + babel.config.js | 70 - bin/webpack | 18 - bin/webpack-dev-server | 18 - config/brakeman.ignore | 18 + config/settings.yml | 4 +- config/webpack/development.js | 5 - config/webpack/environment.js | 3 - config/webpack/production.js | 5 - config/webpack/test.js | 5 - config/webpacker.yml | 92 - package.json | 5 - postcss.config.js | 12 - test/fixtures/club_categories.yml | 14 +- test/fixtures/clubs.yml | 47 +- test/fixtures/content_blocks.yml | 11 - test/fixtures/courses.yml | 11 - test/fixtures/cross_app_sessions.yml | 9 - test/fixtures/event_classifications.yml | 40 +- test/fixtures/events.yml | 23 +- test/fixtures/groups.yml | 34 +- test/fixtures/live_results.yml | 11 - test/fixtures/map_standards.yml | 11 - test/fixtures/maps.yml | 11 - test/fixtures/memberships.yml | 11 - test/fixtures/organizers.yml | 15 +- test/fixtures/pages.yml | 11 - test/fixtures/privileges.yml | 24 +- test/fixtures/results.yml | 11 - test/fixtures/roles.yml | 14 +- test/fixtures/series.yml | 11 - test/fixtures/tokens.yml | 11 - test/fixtures/users.yml | 40 +- test/functional/.gitkeep | 0 test/functional/clubs_controller_test.rb | 7 - .../content_blocks_controller_test.rb | 7 - test/functional/courses_controller_test.rb | 7 - test/functional/events_controller_test.rb | 7 - test/functional/groups_controller_test.rb | 7 - test/functional/home_controller_test.rb | 9 - .../map_standards_controller_test.rb | 7 - test/functional/maps_controller_test.rb | 7 - .../functional/memberships_controller_test.rb | 7 - test/functional/organizers_controller_test.rb | 7 - test/functional/pages_controller_test.rb | 7 - test/functional/privileges_controller_test.rb | 7 - test/functional/results_controller_test.rb | 7 - test/functional/roles_controller_test.rb | 7 - test/functional/series_controller_test.rb | 7 - test/functional/tokens_controller_test.rb | 7 - test/functional/users_controller_test.rb | 7 - test/models/club_test.rb | 12 + test/models/event_test.rb | 39 + test/models/live_result_test.rb | 7 - test/models/user_test.rb | 38 + test/performance/browsing_test.rb | 12 - test/policies/redactor_policy_test.rb | 15 + test/unit/.gitkeep | 0 test/unit/club_category_test.rb | 7 - test/unit/club_test.rb | 7 - test/unit/content_block_test.rb | 7 - test/unit/course_test.rb | 7 - test/unit/cross_app_session_test.rb | 7 - test/unit/event_classification_test.rb | 7 - test/unit/event_test.rb | 7 - test/unit/group_test.rb | 7 - test/unit/helpers/clubs_helper_test.rb | 4 - .../helpers/content_blocks_helper_test.rb | 4 - test/unit/helpers/courses_helper_test.rb | 4 - test/unit/helpers/events_helper_test.rb | 4 - test/unit/helpers/groups_helper_test.rb | 4 - test/unit/helpers/home_helper_test.rb | 4 - .../unit/helpers/map_standards_helper_test.rb | 4 - test/unit/helpers/maps_helper_test.rb | 4 - test/unit/helpers/memberships_helper_test.rb | 4 - test/unit/helpers/organizers_helper_test.rb | 4 - test/unit/helpers/pages_helper_test.rb | 4 - test/unit/helpers/privileges_helper_test.rb | 4 - test/unit/helpers/results_helper_test.rb | 4 - test/unit/helpers/roles_helper_test.rb | 4 - test/unit/helpers/series_helper_test.rb | 4 - test/unit/helpers/tokens_helper_test.rb | 4 - test/unit/helpers/users_helper_test.rb | 4 - test/unit/map_standard_test.rb | 7 - test/unit/map_test.rb | 7 - test/unit/membership_test.rb | 7 - test/unit/organizer_test.rb | 7 - test/unit/page_test.rb | 7 - test/unit/privilege_test.rb | 7 - test/unit/result_test.rb | 7 - test/unit/role_test.rb | 7 - test/unit/series_test.rb | 7 - test/unit/token_test.rb | 7 - test/unit/user_test.rb | 7 - yarn.lock | 6888 ----------------- 130 files changed, 413 insertions(+), 7818 deletions(-) delete mode 100644 .browserslistrc create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/docker-publish.yml create mode 100644 .ruby-version delete mode 100644 app/controllers/content_blocks_controller.rb delete mode 100644 app/controllers/courses_controller.rb delete mode 100644 app/controllers/groups_controller.rb delete mode 100644 app/controllers/map_standards_controller.rb delete mode 100644 app/controllers/memberships_controller.rb delete mode 100644 app/controllers/organizers_controller.rb delete mode 100644 app/controllers/privileges_controller.rb delete mode 100644 app/controllers/roles_controller.rb delete mode 100644 app/controllers/series_controller.rb delete mode 100644 app/helpers/content_blocks_helper.rb delete mode 100644 app/helpers/courses_helper.rb delete mode 100644 app/helpers/events_helper.rb delete mode 100644 app/helpers/groups_helper.rb delete mode 100644 app/helpers/map_standards_helper.rb delete mode 100644 app/helpers/maps_helper.rb delete mode 100644 app/helpers/memberships_helper.rb delete mode 100644 app/helpers/organizers_helper.rb delete mode 100644 app/helpers/pages_helper.rb delete mode 100644 app/helpers/privileges_helper.rb delete mode 100644 app/helpers/results_helper.rb delete mode 100644 app/helpers/roles_helper.rb delete mode 100644 app/helpers/series_helper.rb delete mode 100644 app/helpers/tokens_helper.rb delete mode 100644 app/javascript/packs/application.js delete mode 100644 babel.config.js delete mode 100755 bin/webpack delete mode 100755 bin/webpack-dev-server create mode 100644 config/brakeman.ignore delete mode 100644 config/webpack/development.js delete mode 100644 config/webpack/environment.js delete mode 100644 config/webpack/production.js delete mode 100644 config/webpack/test.js delete mode 100644 config/webpacker.yml delete mode 100644 package.json delete mode 100644 postcss.config.js delete mode 100644 test/fixtures/content_blocks.yml delete mode 100644 test/fixtures/courses.yml delete mode 100644 test/fixtures/cross_app_sessions.yml delete mode 100644 test/fixtures/live_results.yml delete mode 100644 test/fixtures/map_standards.yml delete mode 100644 test/fixtures/maps.yml delete mode 100644 test/fixtures/memberships.yml delete mode 100644 test/fixtures/pages.yml delete mode 100644 test/fixtures/results.yml delete mode 100644 test/fixtures/series.yml delete mode 100644 test/fixtures/tokens.yml delete mode 100644 test/functional/.gitkeep delete mode 100644 test/functional/clubs_controller_test.rb delete mode 100644 test/functional/content_blocks_controller_test.rb delete mode 100644 test/functional/courses_controller_test.rb delete mode 100644 test/functional/events_controller_test.rb delete mode 100644 test/functional/groups_controller_test.rb delete mode 100644 test/functional/home_controller_test.rb delete mode 100644 test/functional/map_standards_controller_test.rb delete mode 100644 test/functional/maps_controller_test.rb delete mode 100644 test/functional/memberships_controller_test.rb delete mode 100644 test/functional/organizers_controller_test.rb delete mode 100644 test/functional/pages_controller_test.rb delete mode 100644 test/functional/privileges_controller_test.rb delete mode 100644 test/functional/results_controller_test.rb delete mode 100644 test/functional/roles_controller_test.rb delete mode 100644 test/functional/series_controller_test.rb delete mode 100644 test/functional/tokens_controller_test.rb delete mode 100644 test/functional/users_controller_test.rb create mode 100644 test/models/club_test.rb create mode 100644 test/models/event_test.rb delete mode 100644 test/models/live_result_test.rb create mode 100644 test/models/user_test.rb delete mode 100644 test/performance/browsing_test.rb create mode 100644 test/policies/redactor_policy_test.rb delete mode 100644 test/unit/.gitkeep delete mode 100644 test/unit/club_category_test.rb delete mode 100644 test/unit/club_test.rb delete mode 100644 test/unit/content_block_test.rb delete mode 100644 test/unit/course_test.rb delete mode 100644 test/unit/cross_app_session_test.rb delete mode 100644 test/unit/event_classification_test.rb delete mode 100644 test/unit/event_test.rb delete mode 100644 test/unit/group_test.rb delete mode 100644 test/unit/helpers/clubs_helper_test.rb delete mode 100644 test/unit/helpers/content_blocks_helper_test.rb delete mode 100644 test/unit/helpers/courses_helper_test.rb delete mode 100644 test/unit/helpers/events_helper_test.rb delete mode 100644 test/unit/helpers/groups_helper_test.rb delete mode 100644 test/unit/helpers/home_helper_test.rb delete mode 100644 test/unit/helpers/map_standards_helper_test.rb delete mode 100644 test/unit/helpers/maps_helper_test.rb delete mode 100644 test/unit/helpers/memberships_helper_test.rb delete mode 100644 test/unit/helpers/organizers_helper_test.rb delete mode 100644 test/unit/helpers/pages_helper_test.rb delete mode 100644 test/unit/helpers/privileges_helper_test.rb delete mode 100644 test/unit/helpers/results_helper_test.rb delete mode 100644 test/unit/helpers/roles_helper_test.rb delete mode 100644 test/unit/helpers/series_helper_test.rb delete mode 100644 test/unit/helpers/tokens_helper_test.rb delete mode 100644 test/unit/helpers/users_helper_test.rb delete mode 100644 test/unit/map_standard_test.rb delete mode 100644 test/unit/map_test.rb delete mode 100644 test/unit/membership_test.rb delete mode 100644 test/unit/organizer_test.rb delete mode 100644 test/unit/page_test.rb delete mode 100644 test/unit/privilege_test.rb delete mode 100644 test/unit/result_test.rb delete mode 100644 test/unit/role_test.rb delete mode 100644 test/unit/series_test.rb delete mode 100644 test/unit/token_test.rb delete mode 100644 test/unit/user_test.rb delete mode 100644 yarn.lock diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index e94f814..0000000 --- a/.browserslistrc +++ /dev/null @@ -1 +0,0 @@ -defaults diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2d68f93 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,115 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + +env: + IMAGE_NAME: core + +jobs: + test: + runs-on: ubuntu-latest + + services: + db: + image: mariadb:10.11 + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: whyjustrun-test + ports: + - 3306:3306 + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=5s + --health-timeout=5s + --health-retries=10 + + env: + RAILS_ENV: test + WJR_DATABASE_NAME: whyjustrun + WJR_DATABASE_TEST_NAME: whyjustrun-test + WJR_DATABASE_HOST: 127.0.0.1 + WJR_DATABASE_USERNAME: root + WJR_DATABASE_PASSWORD: root + WJR_PASSWORD_SALT: test-only-salt + WJR_SECRET_BASE: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_SECRET_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_DEVISE_SECRET_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_RECAPTCHA_SITE_KEY: test + WJR_RECAPTCHA_SECRET_KEY: test + WJR_SMTP_ADDRESS: localhost + WJR_SMTP_DOMAIN: example.com + WJR_SMTP_USER_NAME: test + WJR_SMTP_PASSWORD: test + WJR_DATA_FOLDER: /tmp/whyjustrun-data/ + WJR_DATA_URL: http://localhost/data/ + + steps: + - uses: actions/checkout@v4 + + - name: Install MySQL client headers + run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Prepare test database + run: bundle exec rails db:test:prepare + + - name: Run tests + run: bundle exec rails test + + brakeman: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install MySQL client headers + run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Run Brakeman + run: bundle exec brakeman --no-pager + + push: + runs-on: ubuntu-latest + needs: [test, brakeman] + if: github.event_name == 'push' + + steps: + - uses: actions/checkout@v4 + + - name: Build image + run: docker build . --file Dockerfile --tag $IMAGE_NAME + + - name: Log into GitHub Container Registry + run: echo "${{ secrets.CR_PAT }}" | docker login https://ghcr.io -u ${{ secrets.CR_USER }} --password-stdin + + - name: Push image to GitHub Container Registry + run: | + IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME + + # Change all uppercase to lowercase + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + + # Strip git ref prefix from version + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + + # Strip "v" prefix from tag name + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + + # Use Docker `latest` tag convention + [ "$VERSION" == "master" ] && VERSION=latest + + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + + docker tag $IMAGE_NAME $IMAGE_ID:$VERSION + docker push $IMAGE_ID:$VERSION diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index 145e72a..0000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Docker - -on: - push: - # Publish `master` as Docker `latest` image. - branches: - - master - - # Run tests for any PRs. - pull_request: - -env: - IMAGE_NAME: core - -jobs: - # Push image to GitHub Packages. - # See also https://docs.docker.com/docker-hub/builds/ - push: - runs-on: ubuntu-latest - if: github.event_name == 'push' - - steps: - - uses: actions/checkout@v4 - - - name: Build image - run: docker build . --file Dockerfile --tag $IMAGE_NAME - - - name: Log into GitHub Container Registry - run: echo "${{ secrets.CR_PAT }}" | docker login https://ghcr.io -u ${{ secrets.CR_USER }} --password-stdin - - - name: Push image to GitHub Container Registry - run: | - IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME - - # Change all uppercase to lowercase - IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') - - # Strip git ref prefix from version - VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') - - # Strip "v" prefix from tag name - [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') - - # Use Docker `latest` tag convention - [ "$VERSION" == "master" ] && VERSION=latest - - echo IMAGE_ID=$IMAGE_ID - echo VERSION=$VERSION - - docker tag $IMAGE_NAME $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..a0891f5 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3.4 diff --git a/CLAUDE.md b/CLAUDE.md index 9aebc42..578e42f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,5 +44,5 @@ Test users (password: "password"): admin@example.com, webmaster@example.com, exe - **Cross-app sessions**: Shared authentication with the Clubsite app via `CrossAppSession` model - **Privilege system**: Role-based access with numeric levels (0-100) defined in `config/settings.yml` - **Cron jobs**: Managed by the `whenever` gem -- **Frontend**: Bootstrap with CoffeeScript, Webpacker, jQuery, Leaflet for maps +- **Frontend**: Bootstrap with CoffeeScript, Sprockets asset pipeline, jQuery, Leaflet for maps - **Testing**: Minitest with fixtures in `test/fixtures/` diff --git a/Dockerfile b/Dockerfile index 41747c6..248560b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,6 @@ RUN apk add --no-cache \ build-base \ autoconf \ nodejs \ - yarn \ mariadb-dev \ bash # used in crontab generated by whenever @@ -26,12 +25,8 @@ CMD ["rails", "server", "-b", "0.0.0.0"] FROM dev AS prod -COPY package.json yarn.lock /application/ -RUN yarn install --frozen-lockfile - COPY . /application RUN RAILS_ENV=production \ - NODE_OPTIONS=--openssl-legacy-provider \ SECRET_KEY_BASE=placeholder \ WJR_DATABASE_NAME=placeholder \ WJR_DATABASE_TEST_NAME=placeholder \ diff --git a/Gemfile b/Gemfile index 11972f1..45a072c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,5 @@ source 'http://rubygems.org' -gem 'bundle' - gem 'rails', '~> 7.1' gem 'mysql2', '~> 0.5.3' @@ -16,7 +14,6 @@ gem 'sass-rails', '~> 6.0.0' gem "coffee-script", "~> 2.4.1" gem "coffee-rails", "~> 5.0.0" gem "uglifier", "~> 4.2.0" -gem "webpacker", ">= 5.4.4" gem 'tzinfo-data' # Authentication @@ -58,3 +55,7 @@ gem "pundit", "~> 2.3.2" # Geo gem 'haversine', '~> 0.3.2' + +group :development, :test do + gem 'brakeman', require: false +end diff --git a/Gemfile.lock b/Gemfile.lock index efda3d5..a7a8f83 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,9 +90,9 @@ GEM bootstrap-sass (3.4.1) autoprefixer-rails (>= 5.2.1) sassc (>= 2.0.0) + brakeman (8.0.5) + racc builder (3.3.0) - bundle (0.0.1) - bundler cgi (0.4.2) chronic (0.10.2) coffee-rails (5.0.0) @@ -204,8 +204,6 @@ GEM activesupport (>= 3.0.0) racc (1.8.1) rack (3.2.6) - rack-proxy (0.7.7) - rack rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) @@ -266,7 +264,6 @@ GEM sprockets-rails tilt securerandom (0.4.1) - semantic_range (3.0.0) sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) @@ -288,11 +285,6 @@ GEM useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) - webpacker (5.4.4) - activesupport (>= 5.2) - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -308,7 +300,7 @@ DEPENDENCIES activerecord-session_store (~> 2.1.0) bcrypt (~> 3.1.22) bootstrap-sass (~> 3.4.1) - bundle + brakeman coffee-rails (~> 5.0.0) coffee-script (~> 2.4.1) config (~> 5.5.1) @@ -331,7 +323,6 @@ DEPENDENCIES sass-rails (~> 6.0.0) tzinfo-data uglifier (~> 4.2.0) - webpacker (>= 5.4.4) whenever (~> 1.0.0) BUNDLED WITH diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4cbd47c..22cc58c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,6 @@ class ApplicationController < ActionController::Base include Pundit - protect_from_forgery + protect_from_forgery with: :exception before_action :clear_redirect_club_if_necessary before_action :configure_permitted_parameters, if: :devise_controller? diff --git a/app/controllers/content_blocks_controller.rb b/app/controllers/content_blocks_controller.rb deleted file mode 100644 index 93790c9..0000000 --- a/app/controllers/content_blocks_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class ContentBlocksController < ApplicationController -end diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb deleted file mode 100644 index b7a6f7e..0000000 --- a/app/controllers/courses_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class CoursesController < ApplicationController -end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb deleted file mode 100644 index 2f973a6..0000000 --- a/app/controllers/groups_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class GroupsController < ApplicationController -end diff --git a/app/controllers/map_standards_controller.rb b/app/controllers/map_standards_controller.rb deleted file mode 100644 index 2b44992..0000000 --- a/app/controllers/map_standards_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class MapStandardsController < ApplicationController -end diff --git a/app/controllers/memberships_controller.rb b/app/controllers/memberships_controller.rb deleted file mode 100644 index 37101fc..0000000 --- a/app/controllers/memberships_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class MembershipsController < ApplicationController -end diff --git a/app/controllers/organizers_controller.rb b/app/controllers/organizers_controller.rb deleted file mode 100644 index 48014a2..0000000 --- a/app/controllers/organizers_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class OrganizersController < ApplicationController -end diff --git a/app/controllers/privileges_controller.rb b/app/controllers/privileges_controller.rb deleted file mode 100644 index 6a4a632..0000000 --- a/app/controllers/privileges_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class PrivilegesController < ApplicationController -end diff --git a/app/controllers/results_controller.rb b/app/controllers/results_controller.rb index 690e01e..c850a9d 100644 --- a/app/controllers/results_controller.rb +++ b/app/controllers/results_controller.rb @@ -2,6 +2,9 @@ class ResultsController < ApplicationController before_action :authenticate_user!, :only => [:update_live, :process_result_list] + # Result list uploads come from external tools authenticating over HTTP Basic, + # which cannot supply a CSRF token. + skip_forgery_protection only: [:process_result_list, :update_live_result_list, :update_result_list] def check_event_id id unless Event.exists? id diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb deleted file mode 100644 index 0cb1584..0000000 --- a/app/controllers/roles_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class RolesController < ApplicationController -end diff --git a/app/controllers/series_controller.rb b/app/controllers/series_controller.rb deleted file mode 100644 index ea896a5..0000000 --- a/app/controllers/series_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class SeriesController < ApplicationController -end diff --git a/app/helpers/content_blocks_helper.rb b/app/helpers/content_blocks_helper.rb deleted file mode 100644 index bf17995..0000000 --- a/app/helpers/content_blocks_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ContentBlocksHelper -end diff --git a/app/helpers/courses_helper.rb b/app/helpers/courses_helper.rb deleted file mode 100644 index c159f1e..0000000 --- a/app/helpers/courses_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module CoursesHelper -end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb deleted file mode 100644 index 8a9a878..0000000 --- a/app/helpers/events_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module EventsHelper -end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb deleted file mode 100644 index c091b2f..0000000 --- a/app/helpers/groups_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module GroupsHelper -end diff --git a/app/helpers/map_standards_helper.rb b/app/helpers/map_standards_helper.rb deleted file mode 100644 index 4dd0aaf..0000000 --- a/app/helpers/map_standards_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module MapStandardsHelper -end diff --git a/app/helpers/maps_helper.rb b/app/helpers/maps_helper.rb deleted file mode 100644 index 88ee3d5..0000000 --- a/app/helpers/maps_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module MapsHelper -end diff --git a/app/helpers/memberships_helper.rb b/app/helpers/memberships_helper.rb deleted file mode 100644 index eaf43c7..0000000 --- a/app/helpers/memberships_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module MembershipsHelper -end diff --git a/app/helpers/organizers_helper.rb b/app/helpers/organizers_helper.rb deleted file mode 100644 index cc800b1..0000000 --- a/app/helpers/organizers_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module OrganizersHelper -end diff --git a/app/helpers/pages_helper.rb b/app/helpers/pages_helper.rb deleted file mode 100644 index 2c057fd..0000000 --- a/app/helpers/pages_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module PagesHelper -end diff --git a/app/helpers/privileges_helper.rb b/app/helpers/privileges_helper.rb deleted file mode 100644 index 863b7bc..0000000 --- a/app/helpers/privileges_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module PrivilegesHelper -end diff --git a/app/helpers/results_helper.rb b/app/helpers/results_helper.rb deleted file mode 100644 index c3fd7a5..0000000 --- a/app/helpers/results_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module ResultsHelper -end diff --git a/app/helpers/roles_helper.rb b/app/helpers/roles_helper.rb deleted file mode 100644 index 6b6b6cc..0000000 --- a/app/helpers/roles_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module RolesHelper -end diff --git a/app/helpers/series_helper.rb b/app/helpers/series_helper.rb deleted file mode 100644 index 57f0452..0000000 --- a/app/helpers/series_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module SeriesHelper -end diff --git a/app/helpers/tokens_helper.rb b/app/helpers/tokens_helper.rb deleted file mode 100644 index a16c59d..0000000 --- a/app/helpers/tokens_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module TokensHelper -end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js deleted file mode 100644 index 7c3021d..0000000 --- a/app/javascript/packs/application.js +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint no-console:0 */ -// This file is automatically compiled by Webpack, along with any other files -// present in this directory. You're encouraged to place your actual application logic in -// a relevant structure within app/javascript and only use these pack files to reference -// that code so it'll be compiled. -// -// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate -// layout file, like app/views/layouts/application.html.erb - - -// Uncomment to copy all static images under ../images to the output folder and reference -// them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>) -// or the `imagePath` JavaScript helper below. -// -// const images = require.context('../images', true) -// const imagePath = (name) => images(name, true) - -console.log('Hello World from Webpacker') diff --git a/app/models/event.rb b/app/models/event.rb index bebb730..5fed711 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -159,10 +159,9 @@ def number_of_participants end def to_ics - Time.zone = "UTC" event = Icalendar::Event.new - event.dtstart = date - event.dtend = finish_date + event.dtstart = date.utc + event.dtend = finish_date.utc event.summary = name event.description = strip_tags(description) if has_location @@ -216,7 +215,6 @@ def text_colour(colour) end def to_fullcalendar(prefix_acronym, for_club) - Time.zone = "UTC" out = {} out[:id] = id out[:title] = prefix_acronym ? (club.acronym + ' - ' + name) : name diff --git a/app/models/user.rb b/app/models/user.rb index 780e72f..b2b68b7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -109,6 +109,7 @@ def has_privilege?(desired_privilege, club) def max_privilege_level privilege = Privilege.includes(:user_group) .joins('LEFT JOIN groups ON groups.id = group_id') + .where(user_id: self.id) .order('access_level DESC') .limit(1).take if privilege.nil? diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index 4df1949..0000000 --- a/babel.config.js +++ /dev/null @@ -1,70 +0,0 @@ -module.exports = function(api) { - var validEnv = ['development', 'test', 'production'] - var currentEnv = api.env() - var isDevelopmentEnv = api.env('development') - var isProductionEnv = api.env('production') - var isTestEnv = api.env('test') - - if (!validEnv.includes(currentEnv)) { - throw new Error( - 'Please specify a valid `NODE_ENV` or ' + - '`BABEL_ENV` environment variables. Valid values are "development", ' + - '"test", and "production". Instead, received: ' + - JSON.stringify(currentEnv) + - '.' - ) - } - - return { - presets: [ - isTestEnv && [ - '@babel/preset-env', - { - targets: { - node: 'current' - } - } - ], - (isProductionEnv || isDevelopmentEnv) && [ - '@babel/preset-env', - { - forceAllTransforms: true, - useBuiltIns: 'entry', - corejs: 3, - modules: false, - exclude: ['transform-typeof-symbol'] - } - ] - ].filter(Boolean), - plugins: [ - 'babel-plugin-macros', - '@babel/plugin-syntax-dynamic-import', - isTestEnv && 'babel-plugin-dynamic-import-node', - '@babel/plugin-transform-destructuring', - [ - '@babel/plugin-proposal-class-properties', - { - loose: true - } - ], - [ - '@babel/plugin-proposal-object-rest-spread', - { - useBuiltIns: true - } - ], - [ - '@babel/plugin-transform-runtime', - { - helpers: false - } - ], - [ - '@babel/plugin-transform-regenerator', - { - async: false - } - ] - ].filter(Boolean) - } -} diff --git a/bin/webpack b/bin/webpack deleted file mode 100755 index 1031168..0000000 --- a/bin/webpack +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env ruby - -ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" -ENV["NODE_ENV"] ||= "development" - -require "pathname" -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) - -require "bundler/setup" - -require "webpacker" -require "webpacker/webpack_runner" - -APP_ROOT = File.expand_path("..", __dir__) -Dir.chdir(APP_ROOT) do - Webpacker::WebpackRunner.run(ARGV) -end diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server deleted file mode 100755 index dd96627..0000000 --- a/bin/webpack-dev-server +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env ruby - -ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" -ENV["NODE_ENV"] ||= "development" - -require "pathname" -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) - -require "bundler/setup" - -require "webpacker" -require "webpacker/dev_server_runner" - -APP_ROOT = File.expand_path("..", __dir__) -Dir.chdir(APP_ROOT) do - Webpacker::DevServerRunner.run(ARGV) -end diff --git a/config/brakeman.ignore b/config/brakeman.ignore new file mode 100644 index 0000000..c42ed2e --- /dev/null +++ b/config/brakeman.ignore @@ -0,0 +1,18 @@ +{ + "ignored_warnings": [ + { + "fingerprint": "5d41c99952d21e80fdb86aeae06ea42039c714d2ac801571a150f4c89f548bd9", + "note": "Intentional cross-domain redirect: sends the signed-in user to their club's own domain for the clubsite session handoff. The target host always comes from the clubs table, not from user input." + }, + { + "fingerprint": "ec2fa90ab59aba7a6890bfdc5c76cf7e4b1b241b6dcd1f3a604735709a94af2e", + "note": "Short links exist to redirect to admin-configured external destinations stored in the short_links table." + }, + { + "fingerprint": "98b26f60d776fd41ee6f088c833725145be9aac2d7c5b33780241c273622db42", + "note": "Rails 7.2 end-of-life is a known issue; the framework upgrade is tracked as follow-up work to the clubsite consolidation." + } + ], + "updated": "2026-07-30 00:00:00 +0000", + "brakeman_version": "7.1.1" +} diff --git a/config/settings.yml b/config/settings.yml index 1fb5f7c..6176b8d 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -10,7 +10,8 @@ privileges: delete: 90 edit: 80 planning: 80 - map: + # named "maps" because a "map" key would collide with Enumerable#map on the settings object + maps: delete: 90 edit: 80 viewOCAD: 0 @@ -40,6 +41,7 @@ privileges: edit: 90 user: edit: 100 + merge: 90 host: <%= ENV.fetch('WJR_HOST', 'whyjustrun.ca') %> diff --git a/config/webpack/development.js b/config/webpack/development.js deleted file mode 100644 index c5edff9..0000000 --- a/config/webpack/development.js +++ /dev/null @@ -1,5 +0,0 @@ -process.env.NODE_ENV = process.env.NODE_ENV || 'development' - -const environment = require('./environment') - -module.exports = environment.toWebpackConfig() diff --git a/config/webpack/environment.js b/config/webpack/environment.js deleted file mode 100644 index d16d9af..0000000 --- a/config/webpack/environment.js +++ /dev/null @@ -1,3 +0,0 @@ -const { environment } = require('@rails/webpacker') - -module.exports = environment diff --git a/config/webpack/production.js b/config/webpack/production.js deleted file mode 100644 index be0f53a..0000000 --- a/config/webpack/production.js +++ /dev/null @@ -1,5 +0,0 @@ -process.env.NODE_ENV = process.env.NODE_ENV || 'production' - -const environment = require('./environment') - -module.exports = environment.toWebpackConfig() diff --git a/config/webpack/test.js b/config/webpack/test.js deleted file mode 100644 index c5edff9..0000000 --- a/config/webpack/test.js +++ /dev/null @@ -1,5 +0,0 @@ -process.env.NODE_ENV = process.env.NODE_ENV || 'development' - -const environment = require('./environment') - -module.exports = environment.toWebpackConfig() diff --git a/config/webpacker.yml b/config/webpacker.yml deleted file mode 100644 index a6b1465..0000000 --- a/config/webpacker.yml +++ /dev/null @@ -1,92 +0,0 @@ -# Note: You must restart bin/webpack-dev-server for changes to take effect - -default: &default - source_path: app/javascript - source_entry_path: packs - public_root_path: public - public_output_path: packs - cache_path: tmp/cache/webpacker - webpack_compile_output: true - - # Additional paths webpack should lookup modules - # ['app/assets', 'engine/foo/app/assets'] - additional_paths: [] - - # Reload manifest.json on all requests so we reload latest compiled packs - cache_manifest: false - - # Extract and emit a css file - extract_css: false - - static_assets_extensions: - - .jpg - - .jpeg - - .png - - .gif - - .tiff - - .ico - - .svg - - .eot - - .otf - - .ttf - - .woff - - .woff2 - - extensions: - - .mjs - - .js - - .sass - - .scss - - .css - - .module.sass - - .module.scss - - .module.css - - .png - - .svg - - .gif - - .jpeg - - .jpg - -development: - <<: *default - compile: true - - # Reference: https://webpack.js.org/configuration/dev-server/ - dev_server: - https: false - host: localhost - port: 3035 - public: localhost:3035 - hmr: false - # Inline should be set to true if using HMR - inline: true - overlay: true - compress: true - disable_host_check: true - use_local_ip: false - quiet: false - pretty: false - headers: - 'Access-Control-Allow-Origin': '*' - watch_options: - ignored: '**/node_modules/**' - - -test: - <<: *default - compile: true - - # Compile test packs to a separate directory - public_output_path: packs-test - -production: - <<: *default - - # Production depends on precompilation of packs prior to booting for performance. - compile: false - - # Extract and emit a css file - extract_css: true - - # Cache manifest.json for performance - cache_manifest: true diff --git a/package.json b/package.json deleted file mode 100644 index c3d83e8..0000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "@rails/webpacker": "~5.4.4" - } -} diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index aa5998a..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - plugins: [ - require('postcss-import'), - require('postcss-flexbugs-fixes'), - require('postcss-preset-env')({ - autoprefixer: { - flexbox: 'no-2009' - }, - stage: 3 - }) - ] -} diff --git a/test/fixtures/club_categories.yml b/test/fixtures/club_categories.yml index c63aac0..3ef2599 100644 --- a/test/fixtures/club_categories.yml +++ b/test/fixtures/club_categories.yml @@ -1,11 +1,5 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html +club: + name: Club -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value +national_federation: + name: NationalFederation diff --git a/test/fixtures/clubs.yml b/test/fixtures/clubs.yml index c63aac0..160253e 100644 --- a/test/fixtures/clubs.yml +++ b/test/fixtures/clubs.yml @@ -1,11 +1,38 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html +cluba: + name: Club A Orienteering + acronym: CLUBA + lat: 49.2827 + lng: -123.1207 + country: Canada + timezone: America/Vancouver + visible: true + domain: cluba.test + domain_protocol: https + club_category: club + layout: default -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value +clubb: + name: Club B Orienteering + acronym: CLUBB + lat: 43.6532 + lng: -79.3832 + country: Canada + timezone: America/Toronto + visible: true + domain: clubb.test + domain_protocol: https + redirect_domain: old.clubb.test + club_category: club + layout: other + +federation: + name: National Orienteering Federation + acronym: NOF + lat: 45.4215 + lng: -75.6972 + country: Canada + timezone: America/Toronto + visible: false + domain: federation.test + domain_protocol: https + club_category: national_federation diff --git a/test/fixtures/content_blocks.yml b/test/fixtures/content_blocks.yml deleted file mode 100644 index c63aac0..0000000 --- a/test/fixtures/content_blocks.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value diff --git a/test/fixtures/courses.yml b/test/fixtures/courses.yml deleted file mode 100644 index c63aac0..0000000 --- a/test/fixtures/courses.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value diff --git a/test/fixtures/cross_app_sessions.yml b/test/fixtures/cross_app_sessions.yml deleted file mode 100644 index 22c9cc3..0000000 --- a/test/fixtures/cross_app_sessions.yml +++ /dev/null @@ -1,9 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -one: - user_id: 1 - cross_app_session_id: MyString - -two: - user_id: 1 - cross_app_session_id: MyString diff --git a/test/fixtures/event_classifications.yml b/test/fixtures/event_classifications.yml index c63aac0..393243f 100644 --- a/test/fixtures/event_classifications.yml +++ b/test/fixtures/event_classifications.yml @@ -1,11 +1,29 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value +international: + id: 1 + name: International + iof_3_0_name: International + iof_2_0_3_name: international + +national: + id: 2 + name: National + iof_3_0_name: National + iof_2_0_3_name: national + +regional: + id: 3 + name: Regional + iof_3_0_name: Regional + iof_2_0_3_name: regional + +local: + id: 4 + name: Local + iof_3_0_name: Local + iof_2_0_3_name: local + +club: + id: 5 + name: Club + iof_3_0_name: Club + iof_2_0_3_name: club diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml index c63aac0..bba961d 100644 --- a/test/fixtures/events.yml +++ b/test/fixtures/events.yml @@ -1,11 +1,14 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html +cluba_race: + name: Spring Sprint + club: cluba + event_classification: local + date: 2025-06-15 17:00:00 + finish_date: 2025-06-15 19:00:00 + lat: 49.3 + lng: -123.1 -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value +cluba_training: + name: Evening Training + club: cluba + event_classification: club + date: 2025-07-02 01:30:00 diff --git a/test/fixtures/groups.yml b/test/fixtures/groups.yml index c63aac0..59c759a 100644 --- a/test/fixtures/groups.yml +++ b/test/fixtures/groups.yml @@ -1,11 +1,23 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value +cluba_administrator: + name: Administrator + access_level: 100 + club: cluba + +cluba_webmaster: + name: Webmaster + access_level: 90 + club: cluba + +cluba_executive: + name: Executive + access_level: 80 + club: cluba + +clubb_administrator: + name: Administrator + access_level: 100 + club: clubb + +global_administrator: + name: Global Administrator + access_level: 100 diff --git a/test/fixtures/live_results.yml b/test/fixtures/live_results.yml deleted file mode 100644 index 937a0c0..0000000 --- a/test/fixtures/live_results.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value diff --git a/test/fixtures/map_standards.yml b/test/fixtures/map_standards.yml deleted file mode 100644 index c63aac0..0000000 --- a/test/fixtures/map_standards.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value diff --git a/test/fixtures/maps.yml b/test/fixtures/maps.yml deleted file mode 100644 index c63aac0..0000000 --- a/test/fixtures/maps.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value diff --git a/test/fixtures/memberships.yml b/test/fixtures/memberships.yml deleted file mode 100644 index c63aac0..0000000 --- a/test/fixtures/memberships.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value diff --git a/test/fixtures/organizers.yml b/test/fixtures/organizers.yml index c63aac0..50dcb2a 100644 --- a/test/fixtures/organizers.yml +++ b/test/fixtures/organizers.yml @@ -1,11 +1,4 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value +oscar_on_race: + user: organizer + event: cluba_race + role: organizer diff --git a/test/fixtures/pages.yml b/test/fixtures/pages.yml deleted file mode 100644 index c63aac0..0000000 --- a/test/fixtures/pages.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value diff --git a/test/fixtures/privileges.yml b/test/fixtures/privileges.yml index c63aac0..09f4845 100644 --- a/test/fixtures/privileges.yml +++ b/test/fixtures/privileges.yml @@ -1,11 +1,15 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html +admin_cluba: + user: admin + user_group: cluba_administrator -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value +webmaster_cluba: + user: webmaster + user_group: cluba_webmaster + +executive_cluba: + user: executive + user_group: cluba_executive + +global_admin_global: + user: global_admin + user_group: global_administrator diff --git a/test/fixtures/results.yml b/test/fixtures/results.yml deleted file mode 100644 index c63aac0..0000000 --- a/test/fixtures/results.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml index c63aac0..b1c29f8 100644 --- a/test/fixtures/roles.yml +++ b/test/fixtures/roles.yml @@ -1,11 +1,5 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html +organizer: + name: Organizer -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value +course_planner: + name: Course Planner diff --git a/test/fixtures/series.yml b/test/fixtures/series.yml deleted file mode 100644 index c63aac0..0000000 --- a/test/fixtures/series.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value diff --git a/test/fixtures/tokens.yml b/test/fixtures/tokens.yml deleted file mode 100644 index c63aac0..0000000 --- a/test/fixtures/tokens.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index c63aac0..c32abbc 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,11 +1,29 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html - -# This model initially had no columns defined. If you add columns to the -# model remove the '{}' from the fixture names and add the columns immediately -# below each fixture, per the syntax in the comments below -# -one: {} -# column: value -# -two: {} -# column: value +admin: + name: Alice Admin + email: admin@example.com + club: cluba + +webmaster: + name: Wendy Webmaster + email: webmaster@example.com + club: cluba + +executive: + name: Evan Executive + email: executive@example.com + club: cluba + +member: + name: Mary Member + email: member@example.com + club: cluba + +organizer: + name: Oscar Organizer + email: organizer@example.com + club: cluba + +global_admin: + name: Gary Global + email: global@example.com + club: clubb diff --git a/test/functional/.gitkeep b/test/functional/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/functional/clubs_controller_test.rb b/test/functional/clubs_controller_test.rb deleted file mode 100644 index 65e83c8..0000000 --- a/test/functional/clubs_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class ClubsControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/content_blocks_controller_test.rb b/test/functional/content_blocks_controller_test.rb deleted file mode 100644 index 39d6ea6..0000000 --- a/test/functional/content_blocks_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class ContentBlocksControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/courses_controller_test.rb b/test/functional/courses_controller_test.rb deleted file mode 100644 index dcff259..0000000 --- a/test/functional/courses_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class CoursesControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/events_controller_test.rb b/test/functional/events_controller_test.rb deleted file mode 100644 index 65ff8bb..0000000 --- a/test/functional/events_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class EventsControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/groups_controller_test.rb b/test/functional/groups_controller_test.rb deleted file mode 100644 index ab75c4e..0000000 --- a/test/functional/groups_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class GroupsControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/home_controller_test.rb b/test/functional/home_controller_test.rb deleted file mode 100644 index 0d9bb47..0000000 --- a/test/functional/home_controller_test.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'test_helper' - -class HomeControllerTest < ActionController::TestCase - test "should get index" do - get :index - assert_response :success - end - -end diff --git a/test/functional/map_standards_controller_test.rb b/test/functional/map_standards_controller_test.rb deleted file mode 100644 index a581621..0000000 --- a/test/functional/map_standards_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class MapStandardsControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/maps_controller_test.rb b/test/functional/maps_controller_test.rb deleted file mode 100644 index 9517ea5..0000000 --- a/test/functional/maps_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class MapsControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/memberships_controller_test.rb b/test/functional/memberships_controller_test.rb deleted file mode 100644 index 9e132b0..0000000 --- a/test/functional/memberships_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class MembershipsControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/organizers_controller_test.rb b/test/functional/organizers_controller_test.rb deleted file mode 100644 index cb00e4e..0000000 --- a/test/functional/organizers_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class OrganizersControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/pages_controller_test.rb b/test/functional/pages_controller_test.rb deleted file mode 100644 index bb84322..0000000 --- a/test/functional/pages_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class PagesControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/privileges_controller_test.rb b/test/functional/privileges_controller_test.rb deleted file mode 100644 index 8e16925..0000000 --- a/test/functional/privileges_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class PrivilegesControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/results_controller_test.rb b/test/functional/results_controller_test.rb deleted file mode 100644 index 1143586..0000000 --- a/test/functional/results_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class ResultsControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/roles_controller_test.rb b/test/functional/roles_controller_test.rb deleted file mode 100644 index 6f89162..0000000 --- a/test/functional/roles_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class RolesControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/series_controller_test.rb b/test/functional/series_controller_test.rb deleted file mode 100644 index e29ebaf..0000000 --- a/test/functional/series_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class SeriesControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/tokens_controller_test.rb b/test/functional/tokens_controller_test.rb deleted file mode 100644 index 7f78f94..0000000 --- a/test/functional/tokens_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class TokensControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb deleted file mode 100644 index d23f182..0000000 --- a/test/functional/users_controller_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class UsersControllerTest < ActionController::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/models/club_test.rb b/test/models/club_test.rb new file mode 100644 index 0000000..b24210e --- /dev/null +++ b/test/models/club_test.rb @@ -0,0 +1,12 @@ +require 'test_helper' + +class ClubTest < ActiveSupport::TestCase + test "clubsite_url builds a URL from the club's domain" do + assert_equal 'https://cluba.test/events/view/1', clubs(:cluba).clubsite_url('/events/view/1') + end + + test "visible scope excludes hidden clubs" do + assert_includes Club.visible, clubs(:cluba) + assert_not_includes Club.visible, clubs(:federation) + end +end diff --git a/test/models/event_test.rb b/test/models/event_test.rb new file mode 100644 index 0000000..f44879c --- /dev/null +++ b/test/models/event_test.rb @@ -0,0 +1,39 @@ +require 'test_helper' + +class EventTest < ActiveSupport::TestCase + test "local_date converts to the club's timezone" do + event = events(:cluba_race) + assert_equal 'America/Vancouver', event.local_date.time_zone.name + assert_equal 10, event.local_date.hour # 17:00 UTC is 10:00 in Vancouver (PDT) + end + + test "finish_date defaults to one hour after the start when unset" do + event = events(:cluba_training) + assert_equal event.date + 1.hour, event.finish_date + end + + test "to_ics uses UTC times and does not change the current time zone" do + event = events(:cluba_race) + Time.use_zone('America/Vancouver') do + ics = event.to_ics + assert_equal 'America/Vancouver', Time.zone.name + assert_equal event.date.utc, ics.dtstart + assert_equal event.finish_date.utc, ics.dtend + end + end + + test "to_fullcalendar emits epoch timestamps and does not change the current time zone" do + event = events(:cluba_race) + Time.use_zone('America/Vancouver') do + out = event.to_fullcalendar(false, nil) + assert_equal 'America/Vancouver', Time.zone.name + assert_equal event.date.to_i, out[:start] + assert_equal event.finish_date.to_i, out[:end] + end + end + + test "has_organizer? detects event organizers" do + assert events(:cluba_race).has_organizer?(users(:organizer)) + assert_not events(:cluba_race).has_organizer?(users(:member)) + end +end diff --git a/test/models/live_result_test.rb b/test/models/live_result_test.rb deleted file mode 100644 index 495b214..0000000 --- a/test/models/live_result_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class LiveResultTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 0000000..a69ce4a --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,38 @@ +require 'test_helper' + +class UserTest < ActiveSupport::TestCase + test "privilege_level returns the highest access level for the club" do + assert_equal 100, users(:admin).privilege_level(clubs(:cluba)) + assert_equal 90, users(:webmaster).privilege_level(clubs(:cluba)) + assert_equal 80, users(:executive).privilege_level(clubs(:cluba)) + end + + test "privilege_level is zero without any privileges" do + assert_equal 0, users(:member).privilege_level(clubs(:cluba)) + end + + test "privilege_level is scoped to the club" do + assert_equal 0, users(:admin).privilege_level(clubs(:clubb)) + end + + test "privilege_level includes global groups for any club" do + assert_equal 100, users(:global_admin).privilege_level(clubs(:cluba)) + assert_equal 100, users(:global_admin).privilege_level(clubs(:clubb)) + end + + test "has_privilege? compares against the threshold" do + assert users(:webmaster).has_privilege?(90, clubs(:cluba)) + assert users(:webmaster).has_privilege?(80, clubs(:cluba)) + assert_not users(:webmaster).has_privilege?(100, clubs(:cluba)) + end + + test "max_privilege_level only counts the user's own privileges" do + assert_equal 0, users(:member).max_privilege_level + assert_equal 100, users(:admin).max_privilege_level + assert_not users(:member).has_privilege?(80, nil) + end + + test "being an organizer grants no privilege level" do + assert_equal 0, users(:organizer).privilege_level(clubs(:cluba)) + end +end diff --git a/test/performance/browsing_test.rb b/test/performance/browsing_test.rb deleted file mode 100644 index 3fea27b..0000000 --- a/test/performance/browsing_test.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'test_helper' -require 'rails/performance_test_help' - -class BrowsingTest < ActionDispatch::PerformanceTest - # Refer to the documentation for all available options - # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory] - # :output => 'tmp/performance', :formats => [:flat] } - - def test_homepage - get '/' - end -end diff --git a/test/policies/redactor_policy_test.rb b/test/policies/redactor_policy_test.rb new file mode 100644 index 0000000..ff17766 --- /dev/null +++ b/test/policies/redactor_policy_test.rb @@ -0,0 +1,15 @@ +require 'test_helper' + +class RedactorPolicyTest < ActiveSupport::TestCase + test "users without privileges cannot store files" do + assert_not RedactorPolicy.new(users(:member)).store_file? + end + + test "privileged users can store files" do + assert RedactorPolicy.new(users(:executive)).store_file? + end + + test "event organizers can store files" do + assert RedactorPolicy.new(users(:organizer)).store_file? + end +end diff --git a/test/unit/.gitkeep b/test/unit/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/unit/club_category_test.rb b/test/unit/club_category_test.rb deleted file mode 100644 index 01af105..0000000 --- a/test/unit/club_category_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class ClubCategoryTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/club_test.rb b/test/unit/club_test.rb deleted file mode 100644 index abdfce0..0000000 --- a/test/unit/club_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class ClubTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/content_block_test.rb b/test/unit/content_block_test.rb deleted file mode 100644 index 7a83338..0000000 --- a/test/unit/content_block_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class ContentBlockTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/course_test.rb b/test/unit/course_test.rb deleted file mode 100644 index 4afb5cd..0000000 --- a/test/unit/course_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class CourseTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/cross_app_session_test.rb b/test/unit/cross_app_session_test.rb deleted file mode 100644 index 0fb191c..0000000 --- a/test/unit/cross_app_session_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class CrossAppSessionTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/event_classification_test.rb b/test/unit/event_classification_test.rb deleted file mode 100644 index 2517d3f..0000000 --- a/test/unit/event_classification_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class EventClassificationTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/event_test.rb b/test/unit/event_test.rb deleted file mode 100644 index c6f1566..0000000 --- a/test/unit/event_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class EventTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/group_test.rb b/test/unit/group_test.rb deleted file mode 100644 index 778eb0c..0000000 --- a/test/unit/group_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class GroupTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/helpers/clubs_helper_test.rb b/test/unit/helpers/clubs_helper_test.rb deleted file mode 100644 index fe05630..0000000 --- a/test/unit/helpers/clubs_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class ClubsHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/content_blocks_helper_test.rb b/test/unit/helpers/content_blocks_helper_test.rb deleted file mode 100644 index 701ed28..0000000 --- a/test/unit/helpers/content_blocks_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class ContentBlocksHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/courses_helper_test.rb b/test/unit/helpers/courses_helper_test.rb deleted file mode 100644 index 3befd7e..0000000 --- a/test/unit/helpers/courses_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class CoursesHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/events_helper_test.rb b/test/unit/helpers/events_helper_test.rb deleted file mode 100644 index 2e7567e..0000000 --- a/test/unit/helpers/events_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class EventsHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/groups_helper_test.rb b/test/unit/helpers/groups_helper_test.rb deleted file mode 100644 index cd06333..0000000 --- a/test/unit/helpers/groups_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class GroupsHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/home_helper_test.rb b/test/unit/helpers/home_helper_test.rb deleted file mode 100644 index 4740a18..0000000 --- a/test/unit/helpers/home_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class HomeHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/map_standards_helper_test.rb b/test/unit/helpers/map_standards_helper_test.rb deleted file mode 100644 index 465b7c5..0000000 --- a/test/unit/helpers/map_standards_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class MapStandardsHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/maps_helper_test.rb b/test/unit/helpers/maps_helper_test.rb deleted file mode 100644 index 0a0efcc..0000000 --- a/test/unit/helpers/maps_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class MapsHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/memberships_helper_test.rb b/test/unit/helpers/memberships_helper_test.rb deleted file mode 100644 index d8d243c..0000000 --- a/test/unit/helpers/memberships_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class MembershipsHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/organizers_helper_test.rb b/test/unit/helpers/organizers_helper_test.rb deleted file mode 100644 index 5655958..0000000 --- a/test/unit/helpers/organizers_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class OrganizersHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/pages_helper_test.rb b/test/unit/helpers/pages_helper_test.rb deleted file mode 100644 index 535dfe1..0000000 --- a/test/unit/helpers/pages_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class PagesHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/privileges_helper_test.rb b/test/unit/helpers/privileges_helper_test.rb deleted file mode 100644 index 1338c1a..0000000 --- a/test/unit/helpers/privileges_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class PrivilegesHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/results_helper_test.rb b/test/unit/helpers/results_helper_test.rb deleted file mode 100644 index b39f596..0000000 --- a/test/unit/helpers/results_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class ResultsHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/roles_helper_test.rb b/test/unit/helpers/roles_helper_test.rb deleted file mode 100644 index dfa158b..0000000 --- a/test/unit/helpers/roles_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class RolesHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/series_helper_test.rb b/test/unit/helpers/series_helper_test.rb deleted file mode 100644 index 3a00940..0000000 --- a/test/unit/helpers/series_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class SeriesHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/tokens_helper_test.rb b/test/unit/helpers/tokens_helper_test.rb deleted file mode 100644 index c5eede8..0000000 --- a/test/unit/helpers/tokens_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class TokensHelperTest < ActionView::TestCase -end diff --git a/test/unit/helpers/users_helper_test.rb b/test/unit/helpers/users_helper_test.rb deleted file mode 100644 index 96af37a..0000000 --- a/test/unit/helpers/users_helper_test.rb +++ /dev/null @@ -1,4 +0,0 @@ -require 'test_helper' - -class UsersHelperTest < ActionView::TestCase -end diff --git a/test/unit/map_standard_test.rb b/test/unit/map_standard_test.rb deleted file mode 100644 index 90f0c30..0000000 --- a/test/unit/map_standard_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class MapStandardTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/map_test.rb b/test/unit/map_test.rb deleted file mode 100644 index 033cf0f..0000000 --- a/test/unit/map_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class MapTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/membership_test.rb b/test/unit/membership_test.rb deleted file mode 100644 index 43ba573..0000000 --- a/test/unit/membership_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class MembershipTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/organizer_test.rb b/test/unit/organizer_test.rb deleted file mode 100644 index 9e612b0..0000000 --- a/test/unit/organizer_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class OrganizerTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/page_test.rb b/test/unit/page_test.rb deleted file mode 100644 index e7400b9..0000000 --- a/test/unit/page_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class PageTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/privilege_test.rb b/test/unit/privilege_test.rb deleted file mode 100644 index 1ef5e72..0000000 --- a/test/unit/privilege_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class PrivilegeTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/result_test.rb b/test/unit/result_test.rb deleted file mode 100644 index 3b57edb..0000000 --- a/test/unit/result_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class ResultTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/role_test.rb b/test/unit/role_test.rb deleted file mode 100644 index 11c53a8..0000000 --- a/test/unit/role_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class RoleTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/series_test.rb b/test/unit/series_test.rb deleted file mode 100644 index 6a70905..0000000 --- a/test/unit/series_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class SeriesTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/token_test.rb b/test/unit/token_test.rb deleted file mode 100644 index 272693d..0000000 --- a/test/unit/token_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class TokenTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb deleted file mode 100644 index 82f61e0..0000000 --- a/test/unit/user_test.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class UserTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end -end diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 2a4656c..0000000 --- a/yarn.lock +++ /dev/null @@ -1,6888 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" - integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== - dependencies: - "@babel/highlight" "^7.24.7" - picocolors "^1.0.0" - -"@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" - integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== - dependencies: - "@babel/helper-validator-identifier" "^7.28.5" - js-tokens "^4.0.0" - picocolors "^1.1.1" - -"@babel/code-frame@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" - integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== - dependencies: - "@babel/helper-validator-identifier" "^7.29.7" - js-tokens "^4.0.0" - picocolors "^1.1.1" - -"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.2.tgz#e41928bd33475305c586f6acbbb7e3ade7a6f7f5" - integrity sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ== - -"@babel/compat-data@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629" - integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== - -"@babel/core@^7.15.0": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7" - integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== - dependencies: - "@babel/code-frame" "^7.29.7" - "@babel/generator" "^7.29.7" - "@babel/helper-compilation-targets" "^7.29.7" - "@babel/helper-module-transforms" "^7.29.7" - "@babel/helpers" "^7.29.7" - "@babel/parser" "^7.29.7" - "@babel/template" "^7.29.7" - "@babel/traverse" "^7.29.7" - "@babel/types" "^7.29.7" - "@jridgewell/remapping" "^2.3.5" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/generator@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.0.tgz#f858ddfa984350bc3d3b7f125073c9af6988f18e" - integrity sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw== - dependencies: - "@babel/types" "^7.25.0" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^2.5.1" - -"@babel/generator@^7.29.0": - version "7.29.1" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" - integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== - dependencies: - "@babel/parser" "^7.29.0" - "@babel/types" "^7.29.0" - "@jridgewell/gen-mapping" "^0.3.12" - "@jridgewell/trace-mapping" "^0.3.28" - jsesc "^3.0.2" - -"@babel/generator@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3" - integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ== - dependencies: - "@babel/parser" "^7.29.7" - "@babel/types" "^7.29.7" - "@jridgewell/gen-mapping" "^0.3.12" - "@jridgewell/trace-mapping" "^0.3.28" - jsesc "^3.0.2" - -"@babel/helper-annotate-as-pure@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" - integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg== - dependencies: - "@babel/types" "^7.24.7" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" - integrity sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8", "@babel/helper-compilation-targets@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" - integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== - dependencies: - "@babel/compat-data" "^7.25.2" - "@babel/helper-validator-option" "^7.24.8" - browserslist "^4.23.1" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-compilation-targets@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042" - integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g== - dependencies: - "@babel/compat-data" "^7.29.7" - "@babel/helper-validator-option" "^7.29.7" - browserslist "^4.24.0" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.24.7": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz#a109bf9c3d58dfed83aaf42e85633c89f43a6253" - integrity sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-member-expression-to-functions" "^7.24.8" - "@babel/helper-optimise-call-expression" "^7.24.7" - "@babel/helper-replace-supers" "^7.25.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/traverse" "^7.25.0" - semver "^6.3.1" - -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7", "@babel/helper-create-regexp-features-plugin@^7.25.0": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.2.tgz#24c75974ed74183797ffd5f134169316cd1808d9" - integrity sha512-+wqVGP+DFmqwFD3EH6TMTfUNeqDehV3E/dl+Sd54eaXqm17tEUNbEIn4sVivVowbvUpOtIGxdo3GoXyDH9N/9g== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - regexpu-core "^5.3.1" - semver "^6.3.1" - -"@babel/helper-define-polyfill-provider@^0.6.1", "@babel/helper-define-polyfill-provider@^0.6.2": - version "0.6.2" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" - integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== - dependencies: - "@babel/helper-compilation-targets" "^7.22.6" - "@babel/helper-plugin-utils" "^7.22.5" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - -"@babel/helper-globals@^7.28.0": - version "7.28.0" - resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" - integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== - -"@babel/helper-globals@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b" - integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA== - -"@babel/helper-member-expression-to-functions@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6" - integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA== - dependencies: - "@babel/traverse" "^7.24.8" - "@babel/types" "^7.24.8" - -"@babel/helper-module-imports@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" - integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-module-imports@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" - integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== - dependencies: - "@babel/traverse" "^7.28.6" - "@babel/types" "^7.28.6" - -"@babel/helper-module-imports@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396" - integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== - dependencies: - "@babel/traverse" "^7.29.7" - "@babel/types" "^7.29.7" - -"@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" - integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== - dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-simple-access" "^7.24.7" - "@babel/helper-validator-identifier" "^7.24.7" - "@babel/traverse" "^7.25.2" - -"@babel/helper-module-transforms@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" - integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== - dependencies: - "@babel/helper-module-imports" "^7.28.6" - "@babel/helper-validator-identifier" "^7.28.5" - "@babel/traverse" "^7.28.6" - -"@babel/helper-module-transforms@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae" - integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg== - dependencies: - "@babel/helper-module-imports" "^7.29.7" - "@babel/helper-validator-identifier" "^7.29.7" - "@babel/traverse" "^7.29.7" - -"@babel/helper-optimise-call-expression@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" - integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== - dependencies: - "@babel/types" "^7.24.7" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" - integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== - -"@babel/helper-plugin-utils@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" - integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== - -"@babel/helper-remap-async-to-generator@^7.24.7", "@babel/helper-remap-async-to-generator@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.0.tgz#d2f0fbba059a42d68e5e378feaf181ef6055365e" - integrity sha512-NhavI2eWEIz/H9dbrG0TuOicDhNexze43i5z7lEqwYm0WEZVTwnPpA0EafUTP7+6/W79HWIP2cTe3Z5NiSTVpw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-wrap-function" "^7.25.0" - "@babel/traverse" "^7.25.0" - -"@babel/helper-replace-supers@^7.24.7", "@babel/helper-replace-supers@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz#ff44deac1c9f619523fe2ca1fd650773792000a9" - integrity sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.24.8" - "@babel/helper-optimise-call-expression" "^7.24.7" - "@babel/traverse" "^7.25.0" - -"@babel/helper-simple-access@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" - integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-skip-transparent-expression-wrappers@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" - integrity sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ== - dependencies: - "@babel/traverse" "^7.24.7" - "@babel/types" "^7.24.7" - -"@babel/helper-string-parser@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" - integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== - -"@babel/helper-string-parser@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" - integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== - -"@babel/helper-string-parser@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" - integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== - -"@babel/helper-validator-identifier@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" - integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== - -"@babel/helper-validator-identifier@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" - integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== - -"@babel/helper-validator-identifier@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" - integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== - -"@babel/helper-validator-option@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" - integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== - -"@babel/helper-validator-option@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a" - integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== - -"@babel/helper-wrap-function@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.0.tgz#dab12f0f593d6ca48c0062c28bcfb14ebe812f81" - integrity sha512-s6Q1ebqutSiZnEjaofc/UKDyC4SbzV5n5SrA2Gq8UawLycr3i04f1dX4OzoQVnexm6aOCh37SQNYlJ/8Ku+PMQ== - dependencies: - "@babel/template" "^7.25.0" - "@babel/traverse" "^7.25.0" - "@babel/types" "^7.25.0" - -"@babel/helpers@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607" - integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== - dependencies: - "@babel/template" "^7.29.7" - "@babel/types" "^7.29.7" - -"@babel/highlight@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" - integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== - dependencies: - "@babel/helper-validator-identifier" "^7.24.7" - chalk "^2.4.2" - js-tokens "^4.0.0" - picocolors "^1.0.0" - -"@babel/parser@^7.25.0", "@babel/parser@^7.25.3": - version "7.25.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.3.tgz#91fb126768d944966263f0657ab222a642b82065" - integrity sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw== - dependencies: - "@babel/types" "^7.25.2" - -"@babel/parser@^7.28.6", "@babel/parser@^7.29.0": - version "7.29.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.3.tgz#116f70a77958307fceac27747573032f8a62f88e" - integrity sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA== - dependencies: - "@babel/types" "^7.29.0" - -"@babel/parser@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334" - integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg== - dependencies: - "@babel/types" "^7.29.7" - -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.3": - version "7.25.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.3.tgz#dca427b45a6c0f5c095a1c639dfe2476a3daba7f" - integrity sha512-wUrcsxZg6rqBXG05HG1FPYgsP6EvwF4WpBbxIpWIIYnH8wG0gzx3yZY3dtEHas4sTAOGkbTsc9EGPxwff8lRoA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/traverse" "^7.25.3" - -"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.0.tgz#cd0c583e01369ef51676bdb3d7b603e17d2b3f73" - integrity sha512-Bm4bH2qsX880b/3ziJ8KD711LT7z4u8CFudmjqle65AZj/HNUFhEf90dqYv6O86buWvSBmeQDjv0Tn2aF/bIBA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.0.tgz#749bde80356b295390954643de7635e0dffabe73" - integrity sha512-lXwdNZtTmeVOOFtwM/WDe7yg1PL8sYhRk/XH0FzbR2HDQ0xC+EnQ/JHeoMYSavtU115tnUk0q9CDyq8si+LMAA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz#e4eabdd5109acc399b38d7999b2ef66fc2022f89" - integrity sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/plugin-transform-optional-chaining" "^7.24.7" - -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.0.tgz#3a82a70e7cb7294ad2559465ebcb871dfbf078fb" - integrity sha512-tggFrk1AIShG/RUQbEwt2Tr/E+ObkfwrPjR6BjbRvsx24+PSjK8zrq0GWPNCjo8qpRx4DuJzlcvWJqlm+0h3kw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/traverse" "^7.25.0" - -"@babel/plugin-proposal-class-properties@^7.14.5": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-object-rest-spread@^7.14.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" - integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== - dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.20.7" - -"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": - version "7.21.0-placeholder-for-preset-env.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" - integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-import-assertions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz#2a0b406b5871a20a841240586b1300ce2088a778" - integrity sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-syntax-import-attributes@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca" - integrity sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-syntax-import-meta@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" - integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-arrow-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz#4f6886c11e423bd69f3ce51dbf42424a5f275514" - integrity sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-async-generator-functions@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.0.tgz#b785cf35d73437f6276b1e30439a57a50747bddf" - integrity sha512-uaIi2FdqzjpAMvVqvB51S42oC2JEVgh0LDsGfZVDysWE8LrJtQC2jvKmOqEYThKyB7bDEb7BP1GYWDm7tABA0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-remap-async-to-generator" "^7.25.0" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/traverse" "^7.25.0" - -"@babel/plugin-transform-async-to-generator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" - integrity sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA== - dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-remap-async-to-generator" "^7.24.7" - -"@babel/plugin-transform-block-scoped-functions@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" - integrity sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-block-scoping@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.0.tgz#23a6ed92e6b006d26b1869b1c91d1b917c2ea2ac" - integrity sha512-yBQjYoOjXlFv9nlXb3f1casSHOZkWr29NX+zChVanLg5Nc157CrbEX9D7hxxtTpuFy7Q0YzmmWfJxzvps4kXrQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/plugin-transform-class-properties@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz#256879467b57b0b68c7ddfc5b76584f398cd6834" - integrity sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-class-static-block@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz#c82027ebb7010bc33c116d4b5044fbbf8c05484d" - integrity sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-transform-classes@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz#63122366527d88e0ef61b612554fe3f8c793991e" - integrity sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-compilation-targets" "^7.24.8" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-replace-supers" "^7.25.0" - "@babel/traverse" "^7.25.0" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" - integrity sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/template" "^7.24.7" - -"@babel/plugin-transform-destructuring@^7.14.7", "@babel/plugin-transform-destructuring@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz#c828e814dbe42a2718a838c2a2e16a408e055550" - integrity sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/plugin-transform-dotall-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" - integrity sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-duplicate-keys@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" - integrity sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.0.tgz#809af7e3339466b49c034c683964ee8afb3e2604" - integrity sha512-YLpb4LlYSc3sCUa35un84poXoraOiQucUTTu8X1j18JV+gNa8E0nyUf/CjZ171IRGr4jEguF+vzJU66QZhn29g== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.0" - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/plugin-transform-dynamic-import@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" - integrity sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-transform-exponentiation-operator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" - integrity sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-export-namespace-from@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz#176d52d8d8ed516aeae7013ee9556d540c53f197" - integrity sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-transform-for-of@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" - integrity sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - -"@babel/plugin-transform-function-name@^7.25.1": - version "7.25.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.1.tgz#b85e773097526c1a4fc4ba27322748643f26fc37" - integrity sha512-TVVJVdW9RKMNgJJlLtHsKDTydjZAbwIsn6ySBPQaEAUU5+gVvlJt/9nRmqVbsV/IBanRjzWoaAQKLoamWVOUuA== - dependencies: - "@babel/helper-compilation-targets" "^7.24.8" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/traverse" "^7.25.1" - -"@babel/plugin-transform-json-strings@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" - integrity sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-transform-literals@^7.25.2": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.2.tgz#deb1ad14fc5490b9a65ed830e025bca849d8b5f3" - integrity sha512-HQI+HcTbm9ur3Z2DkO+jgESMAMcYLuN/A7NRw9juzxAezN9AvqvUTnpKP/9kkYANz6u7dFlAyOu44ejuGySlfw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/plugin-transform-logical-assignment-operators@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz#a58fb6eda16c9dc8f9ff1c7b1ba6deb7f4694cb0" - integrity sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-transform-member-expression-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" - integrity sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-modules-amd@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" - integrity sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg== - dependencies: - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-modules-commonjs@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c" - integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA== - dependencies: - "@babel/helper-module-transforms" "^7.24.8" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-simple-access" "^7.24.7" - -"@babel/plugin-transform-modules-systemjs@^7.25.0": - version "7.29.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz#f621105da99919c15cf4bde6fcc7346ef95e7b20" - integrity sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w== - dependencies: - "@babel/helper-module-transforms" "^7.28.6" - "@babel/helper-plugin-utils" "^7.28.6" - "@babel/helper-validator-identifier" "^7.28.5" - "@babel/traverse" "^7.29.0" - -"@babel/plugin-transform-modules-umd@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" - integrity sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A== - dependencies: - "@babel/helper-module-transforms" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" - integrity sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-new-target@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" - integrity sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz#1de4534c590af9596f53d67f52a92f12db984120" - integrity sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-transform-numeric-separator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" - integrity sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-transform-object-rest-spread@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" - integrity sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q== - dependencies: - "@babel/helper-compilation-targets" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.24.7" - -"@babel/plugin-transform-object-super@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" - integrity sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-replace-supers" "^7.24.7" - -"@babel/plugin-transform-optional-catch-binding@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz#00eabd883d0dd6a60c1c557548785919b6e717b4" - integrity sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-transform-optional-chaining@^7.24.7", "@babel/plugin-transform-optional-chaining@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz#bb02a67b60ff0406085c13d104c99a835cdf365d" - integrity sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" - integrity sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-private-methods@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz#e6318746b2ae70a59d023d5cc1344a2ba7a75f5e" - integrity sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-private-property-in-object@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz#4eec6bc701288c1fab5f72e6a4bbc9d67faca061" - integrity sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.24.7" - "@babel/helper-create-class-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-transform-property-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" - integrity sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-regenerator@^7.14.5", "@babel/plugin-transform-regenerator@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" - integrity sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - regenerator-transform "^0.15.2" - -"@babel/plugin-transform-reserved-words@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" - integrity sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-runtime@^7.15.0": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.7.tgz#00a5bfaf8c43cf5c8703a8a6e82b59d9c58f38ca" - integrity sha512-YqXjrk4C+a1kZjewqt+Mmu2UuV1s07y8kqcUf4qYLnoqemhR4gRQikhdAhSVJioMjVTu6Mo6pAbaypEA3jY6fw== - dependencies: - "@babel/helper-module-imports" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.1" - babel-plugin-polyfill-regenerator "^0.6.1" - semver "^6.3.1" - -"@babel/plugin-transform-shorthand-properties@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz#85448c6b996e122fa9e289746140aaa99da64e73" - integrity sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-spread@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" - integrity sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" - -"@babel/plugin-transform-sticky-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" - integrity sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-template-literals@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" - integrity sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-typeof-symbol@^7.24.8": - version "7.24.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz#383dab37fb073f5bfe6e60c654caac309f92ba1c" - integrity sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.8" - -"@babel/plugin-transform-unicode-escapes@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" - integrity sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw== - dependencies: - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-unicode-property-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz#9073a4cd13b86ea71c3264659590ac086605bbcd" - integrity sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-unicode-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" - integrity sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/plugin-transform-unicode-sets-regex@^7.24.7": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz#d40705d67523803a576e29c63cef6e516b858ed9" - integrity sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.24.7" - "@babel/helper-plugin-utils" "^7.24.7" - -"@babel/preset-env@^7.15.0": - version "7.25.3" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.25.3.tgz#0bf4769d84ac51d1073ab4a86f00f30a3a83c67c" - integrity sha512-QsYW7UeAaXvLPX9tdVliMJE7MD7M6MLYVTovRTIwhoYQVFHR1rM4wO8wqAezYi3/BpSD+NzVCZ69R6smWiIi8g== - dependencies: - "@babel/compat-data" "^7.25.2" - "@babel/helper-compilation-targets" "^7.25.2" - "@babel/helper-plugin-utils" "^7.24.8" - "@babel/helper-validator-option" "^7.24.8" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.3" - "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.0" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.0" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.7" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.0" - "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-import-assertions" "^7.24.7" - "@babel/plugin-syntax-import-attributes" "^7.24.7" - "@babel/plugin-syntax-import-meta" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.24.7" - "@babel/plugin-transform-async-generator-functions" "^7.25.0" - "@babel/plugin-transform-async-to-generator" "^7.24.7" - "@babel/plugin-transform-block-scoped-functions" "^7.24.7" - "@babel/plugin-transform-block-scoping" "^7.25.0" - "@babel/plugin-transform-class-properties" "^7.24.7" - "@babel/plugin-transform-class-static-block" "^7.24.7" - "@babel/plugin-transform-classes" "^7.25.0" - "@babel/plugin-transform-computed-properties" "^7.24.7" - "@babel/plugin-transform-destructuring" "^7.24.8" - "@babel/plugin-transform-dotall-regex" "^7.24.7" - "@babel/plugin-transform-duplicate-keys" "^7.24.7" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.0" - "@babel/plugin-transform-dynamic-import" "^7.24.7" - "@babel/plugin-transform-exponentiation-operator" "^7.24.7" - "@babel/plugin-transform-export-namespace-from" "^7.24.7" - "@babel/plugin-transform-for-of" "^7.24.7" - "@babel/plugin-transform-function-name" "^7.25.1" - "@babel/plugin-transform-json-strings" "^7.24.7" - "@babel/plugin-transform-literals" "^7.25.2" - "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" - "@babel/plugin-transform-member-expression-literals" "^7.24.7" - "@babel/plugin-transform-modules-amd" "^7.24.7" - "@babel/plugin-transform-modules-commonjs" "^7.24.8" - "@babel/plugin-transform-modules-systemjs" "^7.25.0" - "@babel/plugin-transform-modules-umd" "^7.24.7" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" - "@babel/plugin-transform-new-target" "^7.24.7" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7" - "@babel/plugin-transform-numeric-separator" "^7.24.7" - "@babel/plugin-transform-object-rest-spread" "^7.24.7" - "@babel/plugin-transform-object-super" "^7.24.7" - "@babel/plugin-transform-optional-catch-binding" "^7.24.7" - "@babel/plugin-transform-optional-chaining" "^7.24.8" - "@babel/plugin-transform-parameters" "^7.24.7" - "@babel/plugin-transform-private-methods" "^7.24.7" - "@babel/plugin-transform-private-property-in-object" "^7.24.7" - "@babel/plugin-transform-property-literals" "^7.24.7" - "@babel/plugin-transform-regenerator" "^7.24.7" - "@babel/plugin-transform-reserved-words" "^7.24.7" - "@babel/plugin-transform-shorthand-properties" "^7.24.7" - "@babel/plugin-transform-spread" "^7.24.7" - "@babel/plugin-transform-sticky-regex" "^7.24.7" - "@babel/plugin-transform-template-literals" "^7.24.7" - "@babel/plugin-transform-typeof-symbol" "^7.24.8" - "@babel/plugin-transform-unicode-escapes" "^7.24.7" - "@babel/plugin-transform-unicode-property-regex" "^7.24.7" - "@babel/plugin-transform-unicode-regex" "^7.24.7" - "@babel/plugin-transform-unicode-sets-regex" "^7.24.7" - "@babel/preset-modules" "0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.4" - babel-plugin-polyfill-regenerator "^0.6.1" - core-js-compat "^3.37.1" - semver "^6.3.1" - -"@babel/preset-modules@0.1.6-no-external-plugins": - version "0.1.6-no-external-plugins" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" - integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/regjsgen@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" - integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== - -"@babel/runtime@^7.15.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4": - version "7.26.10" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.10.tgz#a07b4d8fa27af131a633d7b3524db803eb4764c2" - integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== - dependencies: - regenerator-runtime "^0.14.0" - -"@babel/template@^7.24.7", "@babel/template@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" - integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== - dependencies: - "@babel/code-frame" "^7.24.7" - "@babel/parser" "^7.25.0" - "@babel/types" "^7.25.0" - -"@babel/template@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" - integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== - dependencies: - "@babel/code-frame" "^7.28.6" - "@babel/parser" "^7.28.6" - "@babel/types" "^7.28.6" - -"@babel/template@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700" - integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== - dependencies: - "@babel/code-frame" "^7.29.7" - "@babel/parser" "^7.29.7" - "@babel/types" "^7.29.7" - -"@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.25.0", "@babel/traverse@^7.25.1", "@babel/traverse@^7.25.2", "@babel/traverse@^7.25.3": - version "7.25.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.3.tgz#f1b901951c83eda2f3e29450ce92743783373490" - integrity sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ== - dependencies: - "@babel/code-frame" "^7.24.7" - "@babel/generator" "^7.25.0" - "@babel/parser" "^7.25.3" - "@babel/template" "^7.25.0" - "@babel/types" "^7.25.2" - debug "^4.3.1" - globals "^11.1.0" - -"@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" - integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== - dependencies: - "@babel/code-frame" "^7.29.0" - "@babel/generator" "^7.29.0" - "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.29.0" - "@babel/template" "^7.28.6" - "@babel/types" "^7.29.0" - debug "^4.3.1" - -"@babel/traverse@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d" - integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw== - dependencies: - "@babel/code-frame" "^7.29.7" - "@babel/generator" "^7.29.7" - "@babel/helper-globals" "^7.29.7" - "@babel/parser" "^7.29.7" - "@babel/template" "^7.29.7" - "@babel/types" "^7.29.7" - debug "^4.3.1" - -"@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.4.4": - version "7.25.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.2.tgz#55fb231f7dc958cd69ea141a4c2997e819646125" - integrity sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q== - dependencies: - "@babel/helper-string-parser" "^7.24.8" - "@babel/helper-validator-identifier" "^7.24.7" - to-fast-properties "^2.0.0" - -"@babel/types@^7.28.6", "@babel/types@^7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" - integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== - dependencies: - "@babel/helper-string-parser" "^7.27.1" - "@babel/helper-validator-identifier" "^7.28.5" - -"@babel/types@^7.29.7": - version "7.29.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92" - integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA== - dependencies: - "@babel/helper-string-parser" "^7.29.7" - "@babel/helper-validator-identifier" "^7.29.7" - -"@csstools/convert-colors@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" - integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== - -"@gar/promisify@^1.0.1": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" - integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== - -"@jridgewell/gen-mapping@^0.3.12": - version "0.3.13" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" - integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.5.0" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/remapping@^2.3.5": - version "2.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" - integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/source-map@^0.3.3": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" - integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== - -"@jridgewell/sourcemap-codec@^1.5.0": - version "1.5.5" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" - integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== - -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": - version "0.3.25" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@jridgewell/trace-mapping@^0.3.28": - version "0.3.31" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" - integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - -"@npmcli/fs@^1.0.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" - integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== - dependencies: - "@gar/promisify" "^1.0.1" - semver "^7.3.5" - -"@npmcli/move-file@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" - integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== - dependencies: - mkdirp "^1.0.4" - rimraf "^3.0.2" - -"@rails/webpacker@~5.4.4": - version "5.4.4" - resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-5.4.4.tgz#971a41b987c096c908ce4088accd57c1a9a7e2f7" - integrity sha512-hp9myb2MChYFPfE5Fd/3gF4q2m4wpxC+WWCkxcg6My3HYCptWuqMVXRwrBZ5EJHYiL/WRyMLvVNWwlFV47xhUw== - dependencies: - "@babel/core" "^7.15.0" - "@babel/plugin-proposal-class-properties" "^7.14.5" - "@babel/plugin-proposal-object-rest-spread" "^7.14.7" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-destructuring" "^7.14.7" - "@babel/plugin-transform-regenerator" "^7.14.5" - "@babel/plugin-transform-runtime" "^7.15.0" - "@babel/preset-env" "^7.15.0" - "@babel/runtime" "^7.15.3" - babel-loader "^8.2.2" - babel-plugin-dynamic-import-node "^2.3.3" - babel-plugin-macros "^2.8.0" - case-sensitive-paths-webpack-plugin "^2.4.0" - compression-webpack-plugin "^4.0.1" - core-js "^3.16.2" - css-loader "^3.6.0" - file-loader "^6.2.0" - flatted "^3.2.2" - glob "^7.1.7" - js-yaml "^3.14.1" - mini-css-extract-plugin "^0.9.0" - optimize-css-assets-webpack-plugin "^5.0.8" - path-complete-extname "^1.0.0" - pnp-webpack-plugin "^1.7.0" - postcss-flexbugs-fixes "^4.2.1" - postcss-import "^12.0.1" - postcss-loader "^3.0.0" - postcss-preset-env "^6.7.0" - postcss-safe-parser "^4.0.2" - regenerator-runtime "^0.13.9" - sass "^1.38.0" - sass-loader "10.1.1" - style-loader "^1.3.0" - terser-webpack-plugin "^4.2.3" - webpack "^4.46.0" - webpack-assets-manifest "^3.1.1" - webpack-cli "^3.3.12" - webpack-sources "^1.4.3" - -"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" - integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== - -"@types/node@*": - version "22.0.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.0.1.tgz#98e3ed275e511da5b2391bbcbc88caf2831baa1f" - integrity sha512-RVKWL+s4ax6syie/ev3FXFIs38mke4ZsCDPBcLF2Gu6MbQXKe9Fo9iU0EPUxDB1mDVvC0vCgkV3lKa2f6xIuHg== - dependencies: - undici-types "~6.11.1" - -"@types/parse-json@^4.0.0": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" - integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== - -"@types/q@^1.5.1": - version "1.5.8" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.8.tgz#95f6c6a08f2ad868ba230ead1d2d7f7be3db3837" - integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw== - -"@webassemblyjs/ast@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" - integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== - dependencies: - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - -"@webassemblyjs/floating-point-hex-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" - integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== - -"@webassemblyjs/helper-api-error@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" - integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== - -"@webassemblyjs/helper-buffer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" - integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== - -"@webassemblyjs/helper-code-frame@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" - integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== - dependencies: - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/helper-fsm@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" - integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== - -"@webassemblyjs/helper-module-context@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" - integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== - dependencies: - "@webassemblyjs/ast" "1.9.0" - -"@webassemblyjs/helper-wasm-bytecode@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" - integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== - -"@webassemblyjs/helper-wasm-section@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" - integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - -"@webassemblyjs/ieee754@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" - integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" - integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" - integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== - -"@webassemblyjs/wasm-edit@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" - integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/helper-wasm-section" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-opt" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/wasm-gen@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" - integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wasm-opt@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" - integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - -"@webassemblyjs/wasm-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" - integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wast-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" - integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/floating-point-hex-parser" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-code-frame" "1.9.0" - "@webassemblyjs/helper-fsm" "1.9.0" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/wast-printer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" - integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - "@xtuc/long" "4.2.2" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -acorn@^6.4.1: - version "6.4.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" - integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== - -acorn@^8.8.2: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv-errors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" - integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== - -ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.12.5: - version "6.14.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a" - integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -alphanum-sort@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" - integrity sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ== - -ansi-regex@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" - integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -anymatch@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" - integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== - dependencies: - micromatch "^3.1.4" - normalize-path "^2.1.1" - -anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -aproba@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -arr-diff@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" - integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA== - -arr-flatten@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" - integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== - -arr-union@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" - integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== - -array-buffer-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" - integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== - dependencies: - call-bind "^1.0.5" - is-array-buffer "^3.0.4" - -array-unique@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" - integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== - -array.prototype.reduce@^1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.7.tgz#6aadc2f995af29cb887eb866d981dc85ab6f7dc7" - integrity sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-array-method-boxes-properly "^1.0.0" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - is-string "^1.0.7" - -arraybuffer.prototype.slice@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" - integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== - dependencies: - array-buffer-byte-length "^1.0.1" - call-bind "^1.0.5" - define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.2.1" - get-intrinsic "^1.2.3" - is-array-buffer "^3.0.4" - is-shared-array-buffer "^1.0.2" - -asn1.js@^4.10.1: - version "4.10.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" - integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -assert@^1.1.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.1.tgz#038ab248e4ff078e7bc2485ba6e6388466c78f76" - integrity sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A== - dependencies: - object.assign "^4.1.4" - util "^0.10.4" - -assign-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw== - -async-each@^1.0.1: - version "1.0.6" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.6.tgz#52f1d9403818c179b7561e11a5d1b77eb2160e77" - integrity sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg== - -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - -autoprefixer@^9.6.1: - version "9.8.8" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a" - integrity sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA== - dependencies: - browserslist "^4.12.0" - caniuse-lite "^1.0.30001109" - normalize-range "^0.1.2" - num2fraction "^1.2.2" - picocolors "^0.2.1" - postcss "^7.0.32" - postcss-value-parser "^4.1.0" - -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - -babel-loader@^8.2.2: - version "8.3.0" - resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" - integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== - dependencies: - find-cache-dir "^3.3.1" - loader-utils "^2.0.0" - make-dir "^3.1.0" - schema-utils "^2.6.5" - -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-macros@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz#0f958a7cc6556b1e65344465d99111a1e5e10138" - integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== - dependencies: - "@babel/runtime" "^7.7.2" - cosmiconfig "^6.0.0" - resolve "^1.12.0" - -babel-plugin-polyfill-corejs2@^0.4.10: - version "0.4.11" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" - integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== - dependencies: - "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.6.2" - semver "^6.3.1" - -babel-plugin-polyfill-corejs3@^0.10.1, babel-plugin-polyfill-corejs3@^0.10.4: - version "0.10.4" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" - integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.1" - core-js-compat "^3.36.1" - -babel-plugin-polyfill-regenerator@^0.6.1: - version "0.6.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" - integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.2" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.0.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base@^0.11.1: - version "0.11.2" - resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" - integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== - dependencies: - cache-base "^1.0.1" - class-utils "^0.3.5" - component-emitter "^1.2.1" - define-property "^1.0.0" - isobject "^3.0.1" - mixin-deep "^1.2.0" - pascalcase "^0.1.1" - -baseline-browser-mapping@^2.10.38: - version "2.10.38" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz#c84d093c4bf7325c5053c279d90f153c66526042" - integrity sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw== - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^1.0.0: - version "1.13.1" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" - integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -bindings@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bluebird@^3.5.5: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.0.0, bn.js@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" - integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== - -boolbase@^1.0.0, boolbase@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - -brace-expansion@^1.1.7: - version "1.1.14" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.14.tgz#d9de602370d91347cd9ddad1224d4fd701eb348b" - integrity sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^2.3.1, braces@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" - integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== - dependencies: - arr-flatten "^1.1.0" - array-unique "^0.3.2" - extend-shallow "^2.0.1" - fill-range "^4.0.0" - isobject "^3.0.1" - repeat-element "^1.1.2" - snapdragon "^0.8.1" - snapdragon-node "^2.0.1" - split-string "^3.0.2" - to-regex "^3.0.1" - -braces@~3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -brorand@^1.0.1, brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== - -browserify-aes@^1.0.4, browserify-aes@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" - integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== - dependencies: - bn.js "^5.0.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.3.tgz#7afe4c01ec7ee59a89a558a4b75bd85ae62d4208" - integrity sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw== - dependencies: - bn.js "^5.2.1" - browserify-rsa "^4.1.0" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.5" - hash-base "~3.0" - inherits "^2.0.4" - parse-asn1 "^5.1.7" - readable-stream "^2.3.8" - safe-buffer "^5.2.1" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.23.0, browserslist@^4.23.1, browserslist@^4.6.4: - version "4.23.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.2.tgz#244fe803641f1c19c28c48c4b6ec9736eb3d32ed" - integrity sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA== - dependencies: - caniuse-lite "^1.0.30001640" - electron-to-chromium "^1.4.820" - node-releases "^2.0.14" - update-browserslist-db "^1.1.0" - -browserslist@^4.24.0: - version "4.28.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.4.tgz#dd8b8167a32845ff5f8cd6ce13f5abba16cd04c9" - integrity sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw== - dependencies: - baseline-browser-mapping "^2.10.38" - caniuse-lite "^1.0.30001799" - electron-to-chromium "^1.5.376" - node-releases "^2.0.48" - update-browserslist-db "^1.2.3" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== - -cacache@^12.0.2: - version "12.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" - integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== - dependencies: - bluebird "^3.5.5" - chownr "^1.1.1" - figgy-pudding "^3.5.1" - glob "^7.1.4" - graceful-fs "^4.1.15" - infer-owner "^1.0.3" - lru-cache "^5.1.1" - mississippi "^3.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" - promise-inflight "^1.0.1" - rimraf "^2.6.3" - ssri "^6.0.1" - unique-filename "^1.1.1" - y18n "^4.0.0" - -cacache@^15.0.5: - version "15.3.0" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" - integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== - dependencies: - "@npmcli/fs" "^1.0.0" - "@npmcli/move-file" "^1.0.1" - chownr "^2.0.0" - fs-minipass "^2.0.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^6.0.0" - minipass "^3.1.1" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.2" - mkdirp "^1.0.3" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^8.0.1" - tar "^6.0.2" - unique-filename "^1.1.1" - -cache-base@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" - integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== - dependencies: - collection-visit "^1.0.0" - component-emitter "^1.2.1" - get-value "^2.0.6" - has-value "^1.0.0" - isobject "^3.0.1" - set-value "^2.0.0" - to-object-path "^0.3.0" - union-value "^1.0.0" - unset-value "^1.0.0" - -call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" - integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - set-function-length "^1.2.1" - -call-bind@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" - integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== - dependencies: - call-bind-apply-helpers "^1.0.0" - es-define-property "^1.0.0" - get-intrinsic "^1.2.4" - set-function-length "^1.2.2" - -call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" - integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== - dependencies: - call-bind-apply-helpers "^1.0.2" - get-intrinsic "^1.3.0" - -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ== - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A== - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ== - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase@^5.0.0, camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caniuse-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" - integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== - dependencies: - browserslist "^4.0.0" - caniuse-lite "^1.0.0" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001640: - version "1.0.30001645" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001645.tgz#4c4b7427683dea1170a152cd1654be8d0da7bd71" - integrity sha512-GFtY2+qt91kzyMk6j48dJcwJVq5uTkk71XxE3RtScx7XWRLsO7bU44LOFkOZYR8w9YMS0UhPSYpN/6rAMImmLw== - -caniuse-lite@^1.0.30001799: - version "1.0.30001799" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz#5c909138c27f1a61219d3e092071c1cc7d32dc55" - integrity sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw== - -case-sensitive-paths-webpack-plugin@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" - integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== - -chalk@^2.0, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chokidar@^2.1.8: - version "2.1.8" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" - integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== - dependencies: - anymatch "^2.0.0" - async-each "^1.0.1" - braces "^2.3.2" - glob-parent "^3.1.0" - inherits "^2.0.3" - is-binary-path "^1.0.0" - is-glob "^4.0.0" - normalize-path "^3.0.0" - path-is-absolute "^1.0.0" - readdirp "^2.2.1" - upath "^1.1.1" - optionalDependencies: - fsevents "^1.2.7" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - -chrome-trace-event@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" - integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== - -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.6.tgz#8fe672437d01cd6c4561af5334e0cc50ff1955f7" - integrity sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw== - dependencies: - inherits "^2.0.4" - safe-buffer "^5.2.1" - -class-utils@^0.3.5: - version "0.3.6" - resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" - integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== - dependencies: - arr-union "^3.1.0" - define-property "^0.2.5" - isobject "^3.0.0" - static-extend "^0.1.1" - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - -coa@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" - integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== - dependencies: - "@types/q" "^1.5.1" - chalk "^2.4.1" - q "^1.1.2" - -collection-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" - integrity sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw== - dependencies: - map-visit "^1.0.0" - object-visit "^1.0.0" - -color-convert@^1.9.0, color-convert@^1.9.3: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@^1.0.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-string@^1.6.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== - dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" - -color@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" - integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== - dependencies: - color-convert "^1.9.3" - color-string "^1.6.0" - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== - -component-emitter@^1.2.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" - integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== - -compression-webpack-plugin@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-4.0.1.tgz#33eda97f1170dd38c5556771de10f34245aa0274" - integrity sha512-0mg6PgwTsUe5LEcUrOu3ob32vraDx2VdbMGAT1PARcOV+UJWDYZFdkSo6RbHoGQ061mmmkC7XpRKOlvwm/gzJQ== - dependencies: - cacache "^15.0.5" - find-cache-dir "^3.3.1" - schema-utils "^2.7.0" - serialize-javascript "^4.0.0" - webpack-sources "^1.4.3" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -concat-stream@^1.5.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== - -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - -copy-concurrently@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" - integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== - dependencies: - aproba "^1.1.1" - fs-write-stream-atomic "^1.0.8" - iferr "^0.1.5" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.0" - -copy-descriptor@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" - integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== - -core-js-compat@^3.36.1, core-js-compat@^3.37.1: - version "3.37.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee" - integrity sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg== - dependencies: - browserslist "^4.23.0" - -core-js@^3.16.2: - version "3.37.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.1.tgz#d21751ddb756518ac5a00e4d66499df981a62db9" - integrity sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cosmiconfig@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== - dependencies: - "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" - parse-json "^5.0.0" - path-type "^4.0.0" - yaml "^1.7.2" - -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hash@~1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" - integrity sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - ripemd160 "^2.0.0" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - -cross-spawn@^6.0.5: - version "6.0.6" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.6.tgz#30d0efa0712ddb7eb5a76e1e8721bffafa6b5d57" - integrity sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -css-blank-pseudo@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" - integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w== - dependencies: - postcss "^7.0.5" - -css-color-names@0.0.4, css-color-names@^0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" - integrity sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q== - -css-declaration-sorter@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" - integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== - dependencies: - postcss "^7.0.1" - timsort "^0.3.0" - -css-has-pseudo@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee" - integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^5.0.0-rc.4" - -css-loader@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645" - integrity sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ== - dependencies: - camelcase "^5.3.1" - cssesc "^3.0.0" - icss-utils "^4.1.1" - loader-utils "^1.2.3" - normalize-path "^3.0.0" - postcss "^7.0.32" - postcss-modules-extract-imports "^2.0.0" - postcss-modules-local-by-default "^3.0.2" - postcss-modules-scope "^2.2.0" - postcss-modules-values "^3.0.0" - postcss-value-parser "^4.1.0" - schema-utils "^2.7.0" - semver "^6.3.0" - -css-prefers-color-scheme@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4" - integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg== - dependencies: - postcss "^7.0.5" - -css-select-base-adapter@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" - integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== - -css-select@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" - integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== - dependencies: - boolbase "^1.0.0" - css-what "^3.2.1" - domutils "^1.7.0" - nth-check "^1.0.2" - -css-tree@1.0.0-alpha.37: - version "1.0.0-alpha.37" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" - integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== - dependencies: - mdn-data "2.0.4" - source-map "^0.6.1" - -css-tree@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -css-what@^3.2.1: - version "3.4.2" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" - integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== - -cssdb@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" - integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ== - -cssesc@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" - integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-default@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff" - integrity sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ== - dependencies: - css-declaration-sorter "^4.0.1" - cssnano-util-raw-cache "^4.0.1" - postcss "^7.0.0" - postcss-calc "^7.0.1" - postcss-colormin "^4.0.3" - postcss-convert-values "^4.0.1" - postcss-discard-comments "^4.0.2" - postcss-discard-duplicates "^4.0.2" - postcss-discard-empty "^4.0.1" - postcss-discard-overridden "^4.0.1" - postcss-merge-longhand "^4.0.11" - postcss-merge-rules "^4.0.3" - postcss-minify-font-values "^4.0.2" - postcss-minify-gradients "^4.0.2" - postcss-minify-params "^4.0.2" - postcss-minify-selectors "^4.0.2" - postcss-normalize-charset "^4.0.1" - postcss-normalize-display-values "^4.0.2" - postcss-normalize-positions "^4.0.2" - postcss-normalize-repeat-style "^4.0.2" - postcss-normalize-string "^4.0.2" - postcss-normalize-timing-functions "^4.0.2" - postcss-normalize-unicode "^4.0.1" - postcss-normalize-url "^4.0.1" - postcss-normalize-whitespace "^4.0.2" - postcss-ordered-values "^4.1.2" - postcss-reduce-initial "^4.0.3" - postcss-reduce-transforms "^4.0.2" - postcss-svgo "^4.0.3" - postcss-unique-selectors "^4.0.1" - -cssnano-util-get-arguments@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" - integrity sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw== - -cssnano-util-get-match@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" - integrity sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw== - -cssnano-util-raw-cache@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" - integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== - dependencies: - postcss "^7.0.0" - -cssnano-util-same-parent@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" - integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== - -cssnano@^4.1.10: - version "4.1.11" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.11.tgz#c7b5f5b81da269cb1fd982cb960c1200910c9a99" - integrity sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g== - dependencies: - cosmiconfig "^5.0.0" - cssnano-preset-default "^4.0.8" - is-resolvable "^1.0.0" - postcss "^7.0.0" - -csso@^4.0.2: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - -cyclist@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.2.tgz#673b5f233bf34d8e602b949429f8171d9121bea3" - integrity sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA== - -data-view-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" - integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -data-view-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" - integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -data-view-byte-offset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" - integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -debug@^2.2.0, debug@^2.3.3: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: - version "4.3.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" - integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== - dependencies: - ms "2.1.2" - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== - -decode-uri-component@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" - integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== - -define-data-property@^1.0.1, define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -define-property@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" - integrity sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA== - dependencies: - is-descriptor "^0.1.0" - -define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" - integrity sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA== - dependencies: - is-descriptor "^1.0.0" - -define-property@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" - integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== - dependencies: - is-descriptor "^1.0.2" - isobject "^3.0.1" - -des.js@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" - integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - -detect-file@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" - integrity sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q== - -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - -dom-serializer@0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" - integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== - dependencies: - domelementtype "^2.0.1" - entities "^2.0.0" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -domelementtype@1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" - integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== - -domelementtype@^2.0.1: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domutils@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" - integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== - dependencies: - dom-serializer "0" - domelementtype "1" - -dot-prop@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== - dependencies: - is-obj "^2.0.0" - -dunder-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" - integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== - dependencies: - call-bind-apply-helpers "^1.0.1" - es-errors "^1.3.0" - gopd "^1.2.0" - -duplexify@^3.4.2, duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -electron-to-chromium@^1.4.820: - version "1.5.4" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz#cd477c830dd6fca41fbd5465c1ff6ce08ac22343" - integrity sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA== - -electron-to-chromium@^1.5.376: - version "1.5.378" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz#9ce9591ebed23e8a9119970305084b43a2906454" - integrity sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ== - -elliptic@^6.5.3, elliptic@^6.5.5: - version "6.6.1" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06" - integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -end-of-stream@^1.0.0, end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^4.1.1, enhanced-resolve@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" - integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -errno@^0.1.3, errno@~0.1.7: - version "0.1.8" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.17.2, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.2: - version "1.23.3" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" - integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== - dependencies: - array-buffer-byte-length "^1.0.1" - arraybuffer.prototype.slice "^1.0.3" - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - data-view-buffer "^1.0.1" - data-view-byte-length "^1.0.1" - data-view-byte-offset "^1.0.0" - es-define-property "^1.0.0" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - es-set-tostringtag "^2.0.3" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.6" - get-intrinsic "^1.2.4" - get-symbol-description "^1.0.2" - globalthis "^1.0.3" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" - hasown "^2.0.2" - internal-slot "^1.0.7" - is-array-buffer "^3.0.4" - is-callable "^1.2.7" - is-data-view "^1.0.1" - is-negative-zero "^2.0.3" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.3" - is-string "^1.0.7" - is-typed-array "^1.1.13" - is-weakref "^1.0.2" - object-inspect "^1.13.1" - object-keys "^1.1.1" - object.assign "^4.1.5" - regexp.prototype.flags "^1.5.2" - safe-array-concat "^1.1.2" - safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.9" - string.prototype.trimend "^1.0.8" - string.prototype.trimstart "^1.0.8" - typed-array-buffer "^1.0.2" - typed-array-byte-length "^1.0.1" - typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.6" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.15" - -es-array-method-boxes-properly@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" - integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== - -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" - -es-define-property@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" - integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== - -es-errors@^1.2.1, es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-object-atoms@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" - integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== - dependencies: - es-errors "^1.3.0" - -es-object-atoms@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" - integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== - dependencies: - es-errors "^1.3.0" - -es-set-tostringtag@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" - integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== - dependencies: - get-intrinsic "^1.2.4" - has-tostringtag "^1.0.2" - hasown "^2.0.1" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escalade@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" - integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== - -escalade@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -eslint-scope@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" - integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esrecurse@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -events@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - -expand-brackets@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" - integrity sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA== - dependencies: - debug "^2.3.3" - define-property "^0.2.5" - extend-shallow "^2.0.1" - posix-character-classes "^0.1.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - integrity sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw== - dependencies: - homedir-polyfill "^1.0.1" - -extend-shallow@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" - integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== - dependencies: - is-extendable "^0.1.0" - -extend-shallow@^3.0.0, extend-shallow@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" - integrity sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q== - dependencies: - assign-symbols "^1.0.0" - is-extendable "^1.0.1" - -extglob@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" - integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== - dependencies: - array-unique "^0.3.2" - define-property "^1.0.0" - expand-brackets "^2.1.4" - extend-shallow "^2.0.1" - fragment-cache "^0.2.1" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -figgy-pudding@^3.5.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" - integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== - -file-loader@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" - integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fill-range@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" - integrity sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ== - dependencies: - extend-shallow "^2.0.1" - is-number "^3.0.0" - repeat-string "^1.6.1" - to-regex-range "^2.1.0" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -find-cache-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== - dependencies: - commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" - -find-cache-dir@^3.3.1: - version "3.3.2" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" - integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -findup-sync@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" - integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== - dependencies: - detect-file "^1.0.0" - is-glob "^4.0.0" - micromatch "^3.0.4" - resolve-dir "^1.0.1" - -flatted@^3.2.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" - integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== - -flatten@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" - integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== - -flush-write-stream@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - -for-each@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" - integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== - dependencies: - is-callable "^1.2.7" - -for-in@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" - integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== - -fragment-cache@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" - integrity sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA== - dependencies: - map-cache "^0.2.2" - -from2@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g== - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - -fs-write-stream-atomic@^1.0.8: - version "1.0.10" - resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" - integrity sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA== - dependencies: - graceful-fs "^4.1.2" - iferr "^0.1.5" - imurmurhash "^0.1.4" - readable-stream "1 || 2" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@^1.2.7: - version "1.2.13" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" - integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== - dependencies: - bindings "^1.5.0" - nan "^2.12.1" - -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -function.prototype.name@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" - integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - functions-have-names "^1.2.3" - -functions-have-names@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" - integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== - dependencies: - call-bind-apply-helpers "^1.0.2" - es-define-property "^1.0.1" - es-errors "^1.3.0" - es-object-atoms "^1.1.1" - function-bind "^1.1.2" - get-proto "^1.0.1" - gopd "^1.2.0" - has-symbols "^1.1.0" - hasown "^2.0.2" - math-intrinsics "^1.1.0" - -get-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" - integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== - dependencies: - dunder-proto "^1.0.1" - es-object-atoms "^1.0.0" - -get-symbol-description@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" - integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== - dependencies: - call-bind "^1.0.5" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - -get-value@^2.0.3, get-value@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" - integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== - -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" - integrity sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA== - dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob@^7.1.3, glob@^7.1.4, glob@^7.1.7: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" - integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== - dependencies: - global-prefix "^3.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - integrity sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg== - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" - -global-prefix@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" - integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== - dependencies: - ini "^1.3.5" - kind-of "^6.0.2" - which "^1.3.1" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globalthis@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" - integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== - dependencies: - define-properties "^1.2.1" - gopd "^1.0.1" - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -gopd@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" - integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== - -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -has-bigints@^1.0.1, has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.0.1, has-proto@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" - integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== - -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -has-value@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" - integrity sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q== - dependencies: - get-value "^2.0.3" - has-values "^0.1.4" - isobject "^2.0.0" - -has-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" - integrity sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw== - dependencies: - get-value "^2.0.6" - has-values "^1.0.0" - isobject "^3.0.0" - -has-values@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" - integrity sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ== - -has-values@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" - integrity sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ== - dependencies: - is-number "^3.0.0" - kind-of "^4.0.0" - -has@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" - integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== - -hash-base@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" - integrity sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw== - dependencies: - inherits "^2.0.1" - -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash-base@~3.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - -hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -hex-color-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" - integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== - -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - -homedir-polyfill@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" - integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== - dependencies: - parse-passwd "^1.0.0" - -hsl-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" - integrity sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A== - -hsla-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" - integrity sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA== - -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== - -icss-utils@^4.0.0, icss-utils@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" - integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== - dependencies: - postcss "^7.0.14" - -ieee754@^1.1.4: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -iferr@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" - integrity sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA== - -immutable@^4.0.0: - version "4.3.9" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.9.tgz#c98349e05e9c6e2a4d8fe88ac0b363e0d536b544" - integrity sha512-ObHy4YN7ycwZOUCLI1/6svfyAFu7vL8RhAvVu/bh/RZW9EPlOyDaQ9jDQWCtdqzaXUjgXZCW1migtHE7YI7UGQ== - -import-cwd@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" - integrity sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg== - dependencies: - import-from "^2.1.0" - -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg== - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - -import-fresh@^3.1.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-from@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" - integrity sha512-0vdnLL2wSGnhlRmzHJAg5JHjt1l2vYhzJ7tNLGbeVg0fse56tpGaH0uzH+r9Slej+BSXXEHvBKDEnVSLLE9/+w== - dependencies: - resolve-from "^3.0.0" - -import-local@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" - integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== - dependencies: - pkg-dir "^3.0.0" - resolve-cwd "^2.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -indexes-of@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" - integrity sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA== - -infer-owner@^1.0.3, infer-owner@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== - -ini@^1.3.4, ini@^1.3.5: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -internal-slot@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" - integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== - dependencies: - es-errors "^1.3.0" - hasown "^2.0.0" - side-channel "^1.0.4" - -interpret@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" - integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== - -is-absolute-url@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" - integrity sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg== - -is-accessor-descriptor@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz#3223b10628354644b86260db29b3e693f5ceedd4" - integrity sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA== - dependencies: - hasown "^2.0.0" - -is-array-buffer@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" - integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-arrayish@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" - integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-binary-path@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" - integrity sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q== - dependencies: - binary-extensions "^1.0.0" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-buffer@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-color-stop@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" - integrity sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA== - dependencies: - css-color-names "^0.0.4" - hex-color-regex "^1.1.0" - hsl-regex "^1.0.0" - hsla-regex "^1.0.0" - rgb-regex "^1.0.1" - rgba-regex "^1.0.0" - -is-core-module@^2.13.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" - integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== - dependencies: - hasown "^2.0.2" - -is-data-descriptor@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz#2109164426166d32ea38c405c1e0945d9e6a4eeb" - integrity sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw== - dependencies: - hasown "^2.0.0" - -is-data-view@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" - integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== - dependencies: - is-typed-array "^1.1.13" - -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-descriptor@^0.1.0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.7.tgz#2727eb61fd789dcd5bdf0ed4569f551d2fe3be33" - integrity sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg== - dependencies: - is-accessor-descriptor "^1.0.1" - is-data-descriptor "^1.0.1" - -is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.3.tgz#92d27cb3cd311c4977a4db47df457234a13cb306" - integrity sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw== - dependencies: - is-accessor-descriptor "^1.0.1" - is-data-descriptor "^1.0.1" - -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw== - -is-extendable@^0.1.0, is-extendable@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" - integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== - -is-extendable@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" - integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== - dependencies: - is-plain-object "^2.0.4" - -is-extglob@^2.1.0, is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== - -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - integrity sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw== - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-negative-zero@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" - integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== - -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== - dependencies: - has-tostringtag "^1.0.0" - -is-number@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" - integrity sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg== - dependencies: - kind-of "^3.0.2" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-plain-obj@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" - integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== - -is-plain-object@^2.0.3, is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-resolvable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" - integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== - -is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" - integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== - dependencies: - call-bind "^1.0.7" - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-typed-array@^1.1.13: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - -is-typed-array@^1.1.14: - version "1.1.15" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" - integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== - dependencies: - which-typed-array "^1.1.16" - -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - -is-windows@^1.0.1, is-windows@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" - integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" - integrity sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw== - -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isobject@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" - integrity sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA== - dependencies: - isarray "1.0.0" - -isobject@^3.0.0, isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - -jest-worker@^26.5.0: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" - integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^7.0.0" - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1, js-yaml@^3.14.1: - version "3.15.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.0.tgz#586e5214eafe3e893756a41e979b50d89d3e4a67" - integrity sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" - integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== - -json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json5@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -json5@^2.1.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" - integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== - dependencies: - is-buffer "^1.1.5" - -kind-of@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" - integrity sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw== - dependencies: - is-buffer "^1.1.5" - -kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -klona@^2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" - integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== - -last-call-webpack-plugin@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" - integrity sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w== - dependencies: - lodash "^4.17.5" - webpack-sources "^1.1.0" - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -loader-runner@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" - integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== - -loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" - integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^1.0.1" - -loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - -lodash.get@^4.0: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== - -lodash.has@^4.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" - integrity sha512-rnYUdIo6xRCJnQmbVFEwcxF144erlD+M3YcJUVesflU9paQaE8p+fJDcIQrlMYbxoANFL+AB9hZrzSBBk5PL+g== - -lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== - -lodash@^4.17.5: - version "4.18.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" - integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -make-dir@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -make-dir@^3.0.2, make-dir@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -map-cache@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" - integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg== - -map-visit@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" - integrity sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w== - dependencies: - object-visit "^1.0.0" - -math-intrinsics@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" - integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== - -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - -mdn-data@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" - integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== - -memory-fs@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" - integrity sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -memory-fs@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" - integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: - version "3.1.10" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" - integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - braces "^2.3.1" - define-property "^2.0.2" - extend-shallow "^3.0.2" - extglob "^2.0.4" - fragment-cache "^0.2.1" - kind-of "^6.0.2" - nanomatch "^1.2.9" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.2" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" - -mini-css-extract-plugin@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" - integrity sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A== - dependencies: - loader-utils "^1.1.0" - normalize-url "1.9.1" - schema-utils "^1.0.0" - webpack-sources "^1.1.0" - -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== - -minimatch@^3.1.1: - version "3.1.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" - integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -minipass-collect@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" - integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== - dependencies: - minipass "^3.0.0" - -minipass-flush@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" - integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== - dependencies: - minipass "^3.0.0" - -minipass-pipeline@^1.2.2: - version "1.2.4" - resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" - integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== - dependencies: - minipass "^3.0.0" - -minipass@^3.0.0, minipass@^3.1.1: - version "3.3.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" - integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== - -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mississippi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" - integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^3.0.0" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - -mixin-deep@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" - integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== - dependencies: - for-in "^1.0.2" - is-extendable "^1.0.1" - -mkdirp@^0.5, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: - version "0.5.6" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" - -mkdirp@^1.0.3, mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -move-concurrently@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" - integrity sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ== - dependencies: - aproba "^1.1.1" - copy-concurrently "^1.0.0" - fs-write-stream-atomic "^1.0.8" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.3" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -nan@^2.12.1: - version "2.20.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.20.0.tgz#08c5ea813dd54ed16e5bd6505bf42af4f7838ca3" - integrity sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw== - -nanomatch@^1.2.9: - version "1.2.13" - resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" - integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== - dependencies: - arr-diff "^4.0.0" - array-unique "^0.3.2" - define-property "^2.0.2" - extend-shallow "^3.0.2" - fragment-cache "^0.2.1" - is-windows "^1.0.2" - kind-of "^6.0.2" - object.pick "^1.3.0" - regex-not "^1.0.0" - snapdragon "^0.8.1" - to-regex "^3.0.1" - -neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-libs-browser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -node-releases@^2.0.14: - version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== - -node-releases@^2.0.48: - version "2.0.50" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.50.tgz#597197a852071ce42fc2550e58e223242bcba969" - integrity sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg== - -normalize-path@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" - integrity sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w== - dependencies: - remove-trailing-separator "^1.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - -normalize-url@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" - integrity sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ== - dependencies: - object-assign "^4.0.1" - prepend-http "^1.0.0" - query-string "^4.1.0" - sort-keys "^1.0.0" - -normalize-url@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" - integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== - -nth-check@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" - integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== - dependencies: - boolbase "~1.0.0" - -num2fraction@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" - integrity sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg== - -object-assign@^4.0.1, object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-copy@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" - integrity sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ== - dependencies: - copy-descriptor "^0.1.0" - define-property "^0.2.5" - kind-of "^3.0.3" - -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== - -object-inspect@^1.13.3: - version "1.13.4" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" - integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object-visit@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" - integrity sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA== - dependencies: - isobject "^3.0.0" - -object.assign@^4.1.0, object.assign@^4.1.4, object.assign@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -object.getownpropertydescriptors@^2.1.0: - version "2.1.8" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz#2f1fe0606ec1a7658154ccd4f728504f69667923" - integrity sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A== - dependencies: - array.prototype.reduce "^1.0.6" - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - gopd "^1.0.1" - safe-array-concat "^1.1.2" - -object.pick@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" - integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ== - dependencies: - isobject "^3.0.1" - -object.values@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" - integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -optimize-css-assets-webpack-plugin@^5.0.8: - version "5.0.8" - resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.8.tgz#cbccdcf5a6ef61d4f8cc78cf083a67446e5f402a" - integrity sha512-mgFS1JdOtEGzD8l+EuISqL57cKO+We9GcoiQEmdCWRqqck+FGNmYJtx9qfAPzEz+lRrlThWMuGDaRkI/yWNx/Q== - dependencies: - cssnano "^4.1.10" - last-call-webpack-plugin "^3.0.0" - -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== - -p-limit@^2.0.0, p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -pako@~1.0.5: - version "1.0.11" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -parallel-transform@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" - integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== - dependencies: - cyclist "^1.0.1" - inherits "^2.0.3" - readable-stream "^2.1.5" - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-asn1@^5.0.0, parse-asn1@^5.1.7: - version "5.1.7" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.7.tgz#73cdaaa822125f9647165625eb45f8a051d2df06" - integrity sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg== - dependencies: - asn1.js "^4.10.1" - browserify-aes "^1.2.0" - evp_bytestokey "^1.0.3" - hash-base "~3.0" - pbkdf2 "^3.1.2" - safe-buffer "^5.2.1" - -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw== - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-json@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q== - -pascalcase@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== - -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - -path-complete-extname@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/path-complete-extname/-/path-complete-extname-1.0.0.tgz#f889985dc91000c815515c0bfed06c5acda0752b" - integrity sha512-CVjiWcMRdGU8ubs08YQVzhutOR5DEfO97ipRIlOGMK5Bek5nQySknBpuxVAVJ36hseTNs+vdIcv57ZrWxH7zvg== - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" - integrity sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q== - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pbkdf2@^3.0.3, pbkdf2@^3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.3.tgz#8be674d591d65658113424592a95d1517318dd4b" - integrity sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA== - dependencies: - create-hash "~1.1.3" - create-hmac "^1.1.7" - ripemd160 "=2.0.1" - safe-buffer "^5.2.1" - sha.js "^2.4.11" - to-buffer "^1.2.0" - -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== - -picocolors@^1.0.0, picocolors@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" - integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== - -picocolors@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" - integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.2.tgz#5a942915e26b372dc0f0e6753149a16e6b1c5601" - integrity sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== - -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -pkg-dir@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -pnp-webpack-plugin@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz#65741384f6d8056f36e2255a8d67ffc20866f5c9" - integrity sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg== - dependencies: - ts-pnp "^1.1.6" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== - -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== - -postcss-attribute-case-insensitive@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz#d93e46b504589e94ac7277b0463226c68041a880" - integrity sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^6.0.2" - -postcss-calc@^7.0.1: - version "7.0.5" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" - integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg== - dependencies: - postcss "^7.0.27" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.0.2" - -postcss-color-functional-notation@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0" - integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-color-gray@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547" - integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.5" - postcss-values-parser "^2.0.0" - -postcss-color-hex-alpha@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz#a8d9ca4c39d497c9661e374b9c51899ef0f87388" - integrity sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw== - dependencies: - postcss "^7.0.14" - postcss-values-parser "^2.0.1" - -postcss-color-mod-function@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d" - integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-color-rebeccapurple@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77" - integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-colormin@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" - integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== - dependencies: - browserslist "^4.0.0" - color "^3.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-convert-values@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" - integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== - dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-custom-media@^7.0.8: - version "7.0.8" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" - integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== - dependencies: - postcss "^7.0.14" - -postcss-custom-properties@^8.0.11: - version "8.0.11" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz#2d61772d6e92f22f5e0d52602df8fae46fa30d97" - integrity sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA== - dependencies: - postcss "^7.0.17" - postcss-values-parser "^2.0.1" - -postcss-custom-selectors@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba" - integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-dir-pseudo-class@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2" - integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-discard-comments@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" - integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== - dependencies: - postcss "^7.0.0" - -postcss-discard-duplicates@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" - integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== - dependencies: - postcss "^7.0.0" - -postcss-discard-empty@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" - integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== - dependencies: - postcss "^7.0.0" - -postcss-discard-overridden@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" - integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== - dependencies: - postcss "^7.0.0" - -postcss-double-position-gradients@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e" - integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA== - dependencies: - postcss "^7.0.5" - postcss-values-parser "^2.0.0" - -postcss-env-function@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7" - integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-flexbugs-fixes@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.2.1.tgz#9218a65249f30897deab1033aced8578562a6690" - integrity sha512-9SiofaZ9CWpQWxOwRh1b/r85KD5y7GgvsNt1056k6OYLvWUun0czCvogfJgylC22uJTwW1KzY3Gz65NZRlvoiQ== - dependencies: - postcss "^7.0.26" - -postcss-focus-visible@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" - integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g== - dependencies: - postcss "^7.0.2" - -postcss-focus-within@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680" - integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w== - dependencies: - postcss "^7.0.2" - -postcss-font-variant@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz#42d4c0ab30894f60f98b17561eb5c0321f502641" - integrity sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA== - dependencies: - postcss "^7.0.2" - -postcss-gap-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715" - integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg== - dependencies: - postcss "^7.0.2" - -postcss-image-set-function@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" - integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-import@^12.0.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" - integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== - dependencies: - postcss "^7.0.1" - postcss-value-parser "^3.2.3" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-initial@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.4.tgz#9d32069a10531fe2ecafa0b6ac750ee0bc7efc53" - integrity sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg== - dependencies: - postcss "^7.0.2" - -postcss-lab-function@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e" - integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg== - dependencies: - "@csstools/convert-colors" "^1.4.0" - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-load-config@^2.0.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" - integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== - dependencies: - cosmiconfig "^5.0.0" - import-cwd "^2.0.0" - -postcss-loader@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" - integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== - dependencies: - loader-utils "^1.1.0" - postcss "^7.0.0" - postcss-load-config "^2.0.0" - schema-utils "^1.0.0" - -postcss-logical@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5" - integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA== - dependencies: - postcss "^7.0.2" - -postcss-media-minmax@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" - integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== - dependencies: - postcss "^7.0.2" - -postcss-merge-longhand@^4.0.11: - version "4.0.11" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" - integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== - dependencies: - css-color-names "0.0.4" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - stylehacks "^4.0.0" - -postcss-merge-rules@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" - integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== - dependencies: - browserslist "^4.0.0" - caniuse-api "^3.0.0" - cssnano-util-same-parent "^4.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - vendors "^1.0.0" - -postcss-minify-font-values@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" - integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== - dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-minify-gradients@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" - integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== - dependencies: - cssnano-util-get-arguments "^4.0.0" - is-color-stop "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-minify-params@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" - integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== - dependencies: - alphanum-sort "^1.0.0" - browserslist "^4.0.0" - cssnano-util-get-arguments "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - uniqs "^2.0.0" - -postcss-minify-selectors@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" - integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== - dependencies: - alphanum-sort "^1.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - -postcss-modules-extract-imports@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" - integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== - dependencies: - postcss "^7.0.5" - -postcss-modules-local-by-default@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz#bb14e0cc78279d504dbdcbfd7e0ca28993ffbbb0" - integrity sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw== - dependencies: - icss-utils "^4.1.1" - postcss "^7.0.32" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" - integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== - dependencies: - postcss "^7.0.6" - postcss-selector-parser "^6.0.0" - -postcss-modules-values@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz#5b5000d6ebae29b4255301b4a3a54574423e7f10" - integrity sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg== - dependencies: - icss-utils "^4.0.0" - postcss "^7.0.6" - -postcss-nesting@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.1.tgz#b50ad7b7f0173e5b5e3880c3501344703e04c052" - integrity sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg== - dependencies: - postcss "^7.0.2" - -postcss-normalize-charset@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" - integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== - dependencies: - postcss "^7.0.0" - -postcss-normalize-display-values@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" - integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== - dependencies: - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-positions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" - integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== - dependencies: - cssnano-util-get-arguments "^4.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-repeat-style@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" - integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== - dependencies: - cssnano-util-get-arguments "^4.0.0" - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-string@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" - integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== - dependencies: - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-timing-functions@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" - integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== - dependencies: - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-unicode@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" - integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== - dependencies: - browserslist "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-url@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" - integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== - dependencies: - is-absolute-url "^2.0.0" - normalize-url "^3.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-normalize-whitespace@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" - integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== - dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-ordered-values@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" - integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== - dependencies: - cssnano-util-get-arguments "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-overflow-shorthand@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30" - integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g== - dependencies: - postcss "^7.0.2" - -postcss-page-break@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf" - integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ== - dependencies: - postcss "^7.0.2" - -postcss-place@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62" - integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg== - dependencies: - postcss "^7.0.2" - postcss-values-parser "^2.0.0" - -postcss-preset-env@^6.7.0: - version "6.7.2" - resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.7.2.tgz#58792ed25cff5b997b7274b5401a8868dafa1c52" - integrity sha512-nz+VyUUEB9uAxo5VxI0Gq4E31UjHCG3cUiZW3PzRn7KqkGlAEWuYgb/VLbAitEq7Ooubfix+H2JCm9v+C6hJuw== - dependencies: - autoprefixer "^9.6.1" - browserslist "^4.6.4" - caniuse-lite "^1.0.30000981" - css-blank-pseudo "^0.1.4" - css-has-pseudo "^0.10.0" - css-prefers-color-scheme "^3.1.1" - cssdb "^4.4.0" - postcss "^7.0.17" - postcss-attribute-case-insensitive "^4.0.1" - postcss-color-functional-notation "^2.0.1" - postcss-color-gray "^5.0.0" - postcss-color-hex-alpha "^5.0.3" - postcss-color-mod-function "^3.0.3" - postcss-color-rebeccapurple "^4.0.1" - postcss-custom-media "^7.0.8" - postcss-custom-properties "^8.0.11" - postcss-custom-selectors "^5.1.2" - postcss-dir-pseudo-class "^5.0.0" - postcss-double-position-gradients "^1.0.0" - postcss-env-function "^2.0.2" - postcss-focus-visible "^4.0.0" - postcss-focus-within "^3.0.0" - postcss-font-variant "^4.0.0" - postcss-gap-properties "^2.0.0" - postcss-image-set-function "^3.0.1" - postcss-initial "^3.0.0" - postcss-lab-function "^2.0.1" - postcss-logical "^3.0.0" - postcss-media-minmax "^4.0.0" - postcss-nesting "^7.0.0" - postcss-overflow-shorthand "^2.0.0" - postcss-page-break "^2.0.0" - postcss-place "^4.0.1" - postcss-pseudo-class-any-link "^6.0.0" - postcss-replace-overflow-wrap "^3.0.0" - postcss-selector-matches "^4.0.0" - postcss-selector-not "^4.0.0" - -postcss-pseudo-class-any-link@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1" - integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew== - dependencies: - postcss "^7.0.2" - postcss-selector-parser "^5.0.0-rc.3" - -postcss-reduce-initial@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" - integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== - dependencies: - browserslist "^4.0.0" - caniuse-api "^3.0.0" - has "^1.0.0" - postcss "^7.0.0" - -postcss-reduce-transforms@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" - integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== - dependencies: - cssnano-util-get-match "^4.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-replace-overflow-wrap@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c" - integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw== - dependencies: - postcss "^7.0.2" - -postcss-safe-parser@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz#a6d4e48f0f37d9f7c11b2a581bf00f8ba4870b96" - integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g== - dependencies: - postcss "^7.0.26" - -postcss-selector-matches@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff" - integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" - -postcss-selector-not@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz#263016eef1cf219e0ade9a913780fc1f48204cbf" - integrity sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.2" - -postcss-selector-parser@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270" - integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA== - dependencies: - dot-prop "^5.2.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: - version "5.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" - integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== - dependencies: - cssesc "^2.0.0" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2: - version "6.1.1" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz#5be94b277b8955904476a2400260002ce6c56e38" - integrity sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-svgo@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e" - integrity sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw== - dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - svgo "^1.0.0" - -postcss-unique-selectors@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" - integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== - dependencies: - alphanum-sort "^1.0.0" - postcss "^7.0.0" - uniqs "^2.0.0" - -postcss-value-parser@^3.0.0, postcss-value-parser@^3.2.3: - version "3.3.1" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" - integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== - -postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" - integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg== - dependencies: - flatten "^1.0.2" - indexes-of "^1.0.1" - uniq "^1.0.1" - -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.39" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" - integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== - dependencies: - picocolors "^0.2.1" - source-map "^0.6.1" - -prepend-http@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" - integrity sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== - -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^1.3.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - -punycode@^1.2.4, punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== - -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -q@^1.1.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== - -qs@^6.12.3: - version "6.15.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.2.tgz#fd55426d710403ddccc45e0f9eab16db7727ece9" - integrity sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw== - dependencies: - side-channel "^1.1.0" - -query-string@^4.1.0: - version "4.3.4" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" - integrity sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q== - dependencies: - object-assign "^4.1.0" - strict-uri-encode "^1.0.0" - -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA== - -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" - integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== - dependencies: - pify "^2.3.0" - -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@^2.3.8, readable-stream@~2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.6.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" - integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== - dependencies: - graceful-fs "^4.1.11" - micromatch "^3.1.10" - readable-stream "^2.0.2" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -regenerate-unicode-properties@^10.1.0: - version "10.1.1" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" - integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.13.9: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - -regenerator-transform@^0.15.2: - version "0.15.2" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" - integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== - dependencies: - "@babel/runtime" "^7.8.4" - -regex-not@^1.0.0, regex-not@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" - integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== - dependencies: - extend-shallow "^3.0.2" - safe-regex "^1.1.0" - -regexp.prototype.flags@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" - integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== - dependencies: - call-bind "^1.0.6" - define-properties "^1.2.1" - es-errors "^1.3.0" - set-function-name "^2.0.1" - -regexpu-core@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" - integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== - dependencies: - "@babel/regjsgen" "^0.8.0" - regenerate "^1.4.2" - regenerate-unicode-properties "^10.1.0" - regjsparser "^0.9.1" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" - -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== - dependencies: - jsesc "~0.5.0" - -remove-trailing-separator@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw== - -repeat-element@^1.1.2: - version "1.1.4" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" - integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== - -repeat-string@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" - integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-cwd@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" - integrity sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg== - dependencies: - resolve-from "^3.0.0" - -resolve-dir@^1.0.0, resolve-dir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - integrity sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg== - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-url@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" - integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== - -resolve@^1.1.7, resolve@^1.12.0, resolve@^1.14.2: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -ret@~0.1.10: - version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== - -rgb-regex@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" - integrity sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w== - -rgba-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" - integrity sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg== - -rimraf@^2.5.4, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -ripemd160@=2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" - integrity sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w== - dependencies: - hash-base "^2.0.0" - inherits "^2.0.1" - -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -run-queue@^1.0.0, run-queue@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" - integrity sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg== - dependencies: - aproba "^1.1.1" - -safe-array-concat@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" - integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== - dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" - has-symbols "^1.0.3" - isarray "^2.0.5" - -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-regex-test@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" - integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-regex "^1.1.4" - -safe-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" - integrity sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg== - dependencies: - ret "~0.1.10" - -sass-loader@10.1.1: - version "10.1.1" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.1.1.tgz#4ddd5a3d7638e7949065dd6e9c7c04037f7e663d" - integrity sha512-W6gVDXAd5hR/WHsPicvZdjAWHBcEJ44UahgxcIE196fW2ong0ZHMPO1kZuI5q0VlvMQZh32gpv69PLWQm70qrw== - dependencies: - klona "^2.0.4" - loader-utils "^2.0.0" - neo-async "^2.6.2" - schema-utils "^3.0.0" - semver "^7.3.2" - -sass@^1.38.0: - version "1.77.8" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.8.tgz#9f18b449ea401759ef7ec1752a16373e296b52bd" - integrity sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ== - dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" - source-map-js ">=0.6.2 <2.0.0" - -sax@~1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -schema-utils@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" - integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== - dependencies: - ajv "^6.1.0" - ajv-errors "^1.0.0" - ajv-keywords "^3.1.0" - -schema-utils@^2.6.5, schema-utils@^2.7.0: - version "2.7.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" - integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== - dependencies: - "@types/json-schema" "^7.0.5" - ajv "^6.12.4" - ajv-keywords "^3.5.2" - -schema-utils@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -semver@^5.5.0, semver@^5.6.0: - version "5.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== - -semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.3.2, semver@^7.3.5: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - -serialize-javascript@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== - dependencies: - randombytes "^2.1.0" - -serialize-javascript@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" - integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== - dependencies: - randombytes "^2.1.0" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - -set-function-length@^1.2.1, set-function-length@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - -set-function-name@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" - integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - functions-have-names "^1.2.3" - has-property-descriptors "^1.0.2" - -set-value@^2.0.0, set-value@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" - integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.3" - split-string "^3.0.1" - -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - -sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: - version "2.4.12" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f" - integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w== - dependencies: - inherits "^2.0.4" - safe-buffer "^5.2.1" - to-buffer "^1.2.0" - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== - -side-channel-list@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" - integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - -side-channel-map@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" - integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - -side-channel-weakmap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" - integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== - dependencies: - call-bound "^1.0.2" - es-errors "^1.3.0" - get-intrinsic "^1.2.5" - object-inspect "^1.13.3" - side-channel-map "^1.0.1" - -side-channel@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" - -side-channel@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" - integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== - dependencies: - es-errors "^1.3.0" - object-inspect "^1.13.3" - side-channel-list "^1.0.0" - side-channel-map "^1.0.1" - side-channel-weakmap "^1.0.2" - -simple-swizzle@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" - integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== - dependencies: - is-arrayish "^0.3.1" - -snapdragon-node@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" - integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== - dependencies: - define-property "^1.0.0" - isobject "^3.0.0" - snapdragon-util "^3.0.1" - -snapdragon-util@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" - integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== - dependencies: - kind-of "^3.2.0" - -snapdragon@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" - integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== - dependencies: - base "^0.11.1" - debug "^2.2.0" - define-property "^0.2.5" - extend-shallow "^2.0.1" - map-cache "^0.2.2" - source-map "^0.5.6" - source-map-resolve "^0.5.0" - use "^3.1.0" - -sort-keys@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" - integrity sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg== - dependencies: - is-plain-obj "^1.0.0" - -source-list-map@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== - -"source-map-js@>=0.6.2 <2.0.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" - integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== - -source-map-resolve@^0.5.0: - version "0.5.3" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" - integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - resolve-url "^0.2.1" - source-map-url "^0.4.0" - urix "^0.1.0" - -source-map-support@~0.5.12, source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-url@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" - integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== - -source-map@^0.5.6: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -split-string@^3.0.1, split-string@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" - integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== - dependencies: - extend-shallow "^3.0.0" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -ssri@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" - integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== - dependencies: - figgy-pudding "^3.5.1" - -ssri@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" - integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== - dependencies: - minipass "^3.1.1" - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - -static-extend@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" - integrity sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g== - dependencies: - define-property "^0.2.5" - object-copy "^0.1.0" - -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-each@^1.1.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" - integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== - dependencies: - end-of-stream "^1.1.0" - stream-shift "^1.0.0" - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -stream-shift@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" - integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== - -strict-uri-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" - integrity sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ== - -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string.prototype.trim@^1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" - integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.0" - es-object-atoms "^1.0.0" - -string.prototype.trimend@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" - integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -string.prototype.trimstart@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" - integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -string_decoder@^1.0.0, string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -style-loader@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.3.0.tgz#828b4a3b3b7e7aa5847ce7bae9e874512114249e" - integrity sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q== - dependencies: - loader-utils "^2.0.0" - schema-utils "^2.7.0" - -stylehacks@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" - integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== - dependencies: - browserslist "^4.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -svgo@^1.0.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" - integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== - dependencies: - chalk "^2.4.1" - coa "^2.0.2" - css-select "^2.0.0" - css-select-base-adapter "^0.1.1" - css-tree "1.0.0-alpha.37" - csso "^4.0.2" - js-yaml "^3.13.1" - mkdirp "~0.5.1" - object.values "^1.1.0" - sax "~1.2.4" - stable "^0.1.8" - unquote "~1.1.1" - util.promisify "~1.0.0" - -tapable@^1.0.0, tapable@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tar@^6.0.2: - version "6.2.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" - integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^5.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -terser-webpack-plugin@^1.4.3: - version "1.4.6" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.6.tgz#87fcb6593fd1c977cd09e56143ecd31404600755" - integrity sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA== - dependencies: - cacache "^12.0.2" - find-cache-dir "^2.1.0" - is-wsl "^1.1.0" - schema-utils "^1.0.0" - serialize-javascript "^4.0.0" - source-map "^0.6.1" - terser "^4.1.2" - webpack-sources "^1.4.0" - worker-farm "^1.7.0" - -terser-webpack-plugin@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-4.2.3.tgz#28daef4a83bd17c1db0297070adc07fc8cfc6a9a" - integrity sha512-jTgXh40RnvOrLQNgIkwEKnQ8rmHjHK4u+6UBEi+W+FPmvb+uo+chJXntKe7/3lW5mNysgSWD60KyesnhW8D6MQ== - dependencies: - cacache "^15.0.5" - find-cache-dir "^3.3.1" - jest-worker "^26.5.0" - p-limit "^3.0.2" - schema-utils "^3.0.0" - serialize-javascript "^5.0.1" - source-map "^0.6.1" - terser "^5.3.4" - webpack-sources "^1.4.3" - -terser@^4.1.2: - version "4.8.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" - integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== - dependencies: - commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" - -terser@^5.3.4: - version "5.31.3" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.3.tgz#b24b7beb46062f4653f049eea4f0cd165d0f0c38" - integrity sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA== - dependencies: - "@jridgewell/source-map" "^0.3.3" - acorn "^8.8.2" - commander "^2.20.0" - source-map-support "~0.5.20" - -through2@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -timers-browserify@^2.0.4: - version "2.0.12" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" - integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== - dependencies: - setimmediate "^1.0.4" - -timsort@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" - integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A== - -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA== - -to-buffer@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.1.tgz#2ce650cdb262e9112a18e65dc29dcb513c8155e0" - integrity sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ== - dependencies: - isarray "^2.0.5" - safe-buffer "^5.2.1" - typed-array-buffer "^1.0.3" - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - -to-object-path@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" - integrity sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg== - dependencies: - kind-of "^3.0.2" - -to-regex-range@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" - integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg== - dependencies: - is-number "^3.0.0" - repeat-string "^1.6.1" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-regex@^3.0.1, to-regex@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" - integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== - dependencies: - define-property "^2.0.2" - extend-shallow "^3.0.2" - regex-not "^1.0.2" - safe-regex "^1.1.0" - -ts-pnp@^1.1.6: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" - integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== - -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw== - -typed-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" - integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - is-typed-array "^1.1.13" - -typed-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" - integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== - dependencies: - call-bound "^1.0.3" - es-errors "^1.3.0" - is-typed-array "^1.1.14" - -typed-array-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" - integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== - dependencies: - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - -typed-array-byte-offset@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" - integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - -typed-array-length@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" - integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== - dependencies: - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - possible-typed-array-names "^1.0.0" - -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== - -unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== - dependencies: - call-bind "^1.0.2" - has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" - -undici-types@~6.11.1: - version "6.11.1" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.11.1.tgz#432ea6e8efd54a48569705a699e62d8f4981b197" - integrity sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ== - -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== - -union-value@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" - integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== - dependencies: - arr-union "^3.1.0" - get-value "^2.0.6" - is-extendable "^0.1.1" - set-value "^2.0.1" - -uniq@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" - integrity sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA== - -uniqs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" - integrity sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ== - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - -unquote@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" - integrity sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg== - -unset-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" - integrity sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ== - dependencies: - has-value "^0.3.1" - isobject "^3.0.0" - -upath@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" - integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== - -update-browserslist-db@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" - integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== - dependencies: - escalade "^3.1.2" - picocolors "^1.0.1" - -update-browserslist-db@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" - integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.1" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -urix@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" - integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg== - -url@^0.11.0: - version "0.11.4" - resolved "https://registry.yarnpkg.com/url/-/url-0.11.4.tgz#adca77b3562d56b72746e76b330b7f27b6721f3c" - integrity sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg== - dependencies: - punycode "^1.4.1" - qs "^6.12.3" - -use@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" - integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -util.promisify@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" - integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.2" - has-symbols "^1.0.1" - object.getownpropertydescriptors "^2.1.0" - -util@^0.10.4: - version "0.10.4" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" - integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== - dependencies: - inherits "2.0.3" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - -v8-compile-cache@^2.1.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" - integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== - -vendors@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" - integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== - -vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - -watchpack-chokidar2@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" - integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww== - dependencies: - chokidar "^2.1.8" - -watchpack@^1.7.4: - version "1.7.5" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" - integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== - dependencies: - graceful-fs "^4.1.2" - neo-async "^2.5.0" - optionalDependencies: - chokidar "^3.4.1" - watchpack-chokidar2 "^2.0.1" - -webpack-assets-manifest@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/webpack-assets-manifest/-/webpack-assets-manifest-3.1.1.tgz#39bbc3bf2ee57fcd8ba07cda51c9ba4a3c6ae1de" - integrity sha512-JV9V2QKc5wEWQptdIjvXDUL1ucbPLH2f27toAY3SNdGZp+xSaStAgpoMcvMZmqtFrBc9a5pTS1058vxyMPOzRQ== - dependencies: - chalk "^2.0" - lodash.get "^4.0" - lodash.has "^4.0" - mkdirp "^0.5" - schema-utils "^1.0.0" - tapable "^1.0.0" - webpack-sources "^1.0.0" - -webpack-cli@^3.3.12: - version "3.3.12" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a" - integrity sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag== - dependencies: - chalk "^2.4.2" - cross-spawn "^6.0.5" - enhanced-resolve "^4.1.1" - findup-sync "^3.0.0" - global-modules "^2.0.0" - import-local "^2.0.0" - interpret "^1.4.0" - loader-utils "^1.4.0" - supports-color "^6.1.0" - v8-compile-cache "^2.1.1" - yargs "^13.3.2" - -webpack-sources@^1.0.0, webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack@^4.46.0: - version "4.47.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.47.0.tgz#8b8a02152d7076aeb03b61b47dad2eeed9810ebc" - integrity sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/wasm-edit" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.4.1" - ajv "^6.10.2" - ajv-keywords "^3.4.1" - chrome-trace-event "^1.0.2" - enhanced-resolve "^4.5.0" - eslint-scope "^4.0.3" - json-parse-better-errors "^1.0.2" - loader-runner "^2.4.0" - loader-utils "^1.2.3" - memory-fs "^0.4.1" - micromatch "^3.1.10" - mkdirp "^0.5.3" - neo-async "^2.6.1" - node-libs-browser "^2.2.1" - schema-utils "^1.0.0" - tapable "^1.1.3" - terser-webpack-plugin "^1.4.3" - watchpack "^1.7.4" - webpack-sources "^1.4.1" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which-module@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" - integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== - -which-typed-array@^1.1.14, which-typed-array@^1.1.15: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - -which-typed-array@^1.1.16: - version "1.1.19" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" - integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.8" - call-bound "^1.0.4" - for-each "^0.3.5" - get-proto "^1.0.1" - gopd "^1.2.0" - has-tostringtag "^1.0.2" - -which@^1.2.14, which@^1.2.9, which@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -worker-farm@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" - integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== - dependencies: - errno "~0.1.7" - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -xtend@^4.0.0, xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -y18n@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" - integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.7.2: - version "1.10.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.3.tgz#76e407ed95c42684fb8e14641e5de62fe65bbcb3" - integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA== - -yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^13.3.2: - version "13.3.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 3eb08bdb1e9ae393c5fc6eaaecdd33b3007359fe Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Thu, 30 Jul 2026 21:50:28 +0200 Subject: [PATCH 02/16] Add club website tenancy platform Serve club domains from this app alongside the apex site, in preparation for folding the CakePHP Clubsite in: - Requests on non-apex hosts route to a new Clubsite:: namespace via a domain constraint; a catch-all keeps club domains out of apex routes - Clubsite::BaseController resolves the club from the Host header (301s redirect_domain, 404s unknown hosts), runs requests in the club's timezone, and selects the club's layout - Port the default/other/embed/printable layouts, menu highlighting, club branding resources (header image, logo, custom CSS), and per-series color CSS - Fold clubsite frontend assets into Sprockets: vendored libraries, the wjr modules converted from AMD to plain scripts (RequireJS dropped), and five clubsite bundles; apex bundles unchanged - Per-club robots.txt honoring club visibility; apex robots.txt moves to a controller since static files are served for every host - IOF XML, event feed, maps, and redactor API routes become reachable on club domains as well as the apex - Google Maps API key moves from source to configuration - Production host allowlisting shifts to nginx (generated per-club vhosts plus a default_server); development supports *.localhost club domains and seeds a demo club - Integration tests for tenancy, layout selection, redirects, robots --- .env.core.sample | 3 + .../alpha-horizontal.png | Bin 0 -> 3635 bytes .../clubsite/bootstrap-colorpicker/alpha.png | Bin 0 -> 3271 bytes .../bootstrap-colorpicker/hue-horizontal.png | Bin 0 -> 2837 bytes .../clubsite/bootstrap-colorpicker/hue.png | Bin 0 -> 2972 bytes .../bootstrap-colorpicker/saturation.png | Bin 0 -> 8817 bytes app/assets/images/clubsite/fancybox/blank.gif | Bin 0 -> 43 bytes .../clubsite/fancybox/fancybox_loading.gif | Bin 0 -> 3866 bytes .../clubsite/fancybox/fancybox_overlay.png | Bin 0 -> 1003 bytes .../clubsite/fancybox/fancybox_sprite.png | Bin 0 -> 1362 bytes .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20290 bytes .../fonts/glyphicons-halflings-regular.svg | 229 + .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 41236 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23292 bytes app/assets/javascripts/application.js | 20 +- app/assets/javascripts/clubsite.js | 38 + .../javascripts/clubsite/wjr/binding.js | 28 + .../javascripts/clubsite/wjr/calendar.js | 56 + app/assets/javascripts/clubsite/wjr/club.js | 8 + .../javascripts/clubsite/wjr/clubsite.js | 156 + .../clubsite/wjr/edit-event-courses.js | 55 + .../clubsite/wjr/edit-event-organizers.js | 65 + .../javascripts/clubsite/wjr/editable.js | 76 + .../javascripts/clubsite/wjr/event-list.js | 244 + .../javascripts/clubsite/wjr/flickr-photos.js | 67 + app/assets/javascripts/clubsite/wjr/forms.js | 103 + app/assets/javascripts/clubsite/wjr/iof.js | 142 + app/assets/javascripts/clubsite/wjr/map.js | 184 + .../clubsite/wjr/register-others.js | 48 + .../javascripts/clubsite/wjr/result-editor.js | 178 + .../javascripts/clubsite/wjr/result-list.js | 43 + app/assets/javascripts/clubsite/wjr/utils.js | 16 + app/assets/javascripts/clubsite/wjr/wjr.js | 22 + .../clubsite/wjr/wysiwyg.redactor.js | 20 + app/assets/stylesheets/application.css | 19 +- app/assets/stylesheets/clubsite.css | 12 + app/assets/stylesheets/clubsite/embed.css | 3 + app/assets/stylesheets/clubsite/other.css | 23 + app/assets/stylesheets/clubsite/printable.css | 54 + .../stylesheets/clubsite/whyjustrun.css | 503 + app/assets/stylesheets/clubsite/wysiwyg.css | 6 + app/assets/stylesheets/clubsite_embed.css | 6 + app/assets/stylesheets/clubsite_other.css | 7 + app/assets/stylesheets/clubsite_printable.css | 6 + app/constraints/club_domain_constraint.rb | 10 + app/controllers/application_controller.rb | 2 +- app/controllers/clubsite/base_controller.rb | 62 + app/controllers/clubsite/errors_controller.rb | 8 + app/controllers/clubsite/pages_controller.rb | 6 + app/controllers/clubsite/robots_controller.rb | 10 + app/controllers/home_controller.rb | 10 + app/helpers/clubsite/layout_helper.rb | 21 + app/helpers/clubsite/menu_helper.rb | 19 + app/models/current.rb | 3 + app/models/resource.rb | 41 + app/views/clubsite/pages/home.html.erb | 5 + app/views/layouts/clubsite/_flash.html.erb | 1 + app/views/layouts/clubsite/_head.html.erb | 15 + .../layouts/clubsite/_series_css.html.erb | 9 + app/views/layouts/clubsite/default.html.erb | 77 + app/views/layouts/clubsite/embed.html.erb | 14 + app/views/layouts/clubsite/other.html.erb | 55 + app/views/layouts/clubsite/printable.html.erb | 15 + config/brakeman.ignore | 4 + config/environments/development.rb | 4 + config/environments/production.rb | 11 +- config/environments/test.rb | 3 + config/initializers/assets.rb | 1 + config/initializers/mime_types.rb | 4 + config/routes.rb | 52 +- config/settings.yml | 5 + docker-compose.yml | 2 + lib/tasks/db.rake | 23 +- public/robots.txt | 5 - test/integration/clubsite_tenancy_test.rb | 57 + test/integration/robots_test.rb | 24 + .../clubsite/bootstrap-colorpicker.js | 1 + .../clubsite/bootstrap-datetimepicker.js | 2 + .../clubsite/bootstrap-typeahead.js | 337 + .../assets/javascripts/clubsite/bootstrap.js | 2006 +++ .../javascripts/clubsite/fullcalendar.js | 114 + .../javascripts/clubsite/jquery.fancybox.js | 46 + .../javascripts/clubsite/jquery.jeditable.js | 38 + vendor/assets/javascripts/clubsite/jquery.js | 10337 ++++++++++++++++ .../javascripts/clubsite/jquery.ketchup.js | 41 + .../clubsite/jquery.placeholder.js | 2 + .../javascripts/clubsite/ketchup-bootstrap.js | 86 + .../assets/javascripts/clubsite/knockout.js | 94 + vendor/assets/javascripts/clubsite/ladda.js | 157 + .../javascripts/clubsite/markerclusterer.js | 1321 ++ vendor/assets/javascripts/clubsite/moment.js | 7 + vendor/assets/javascripts/clubsite/spin.js | 218 + .../assets/javascripts/clubsite/underscore.js | 6 + .../clubsite/bootstrap-colorpicker.css.erb | 214 + .../clubsite/bootstrap-datetimepicker.min.css | 5 + .../clubsite/bootstrap.min.css.erb | 7 + .../stylesheets/clubsite/fullcalendar.css | 618 + .../clubsite/jquery.fancybox.css.erb | 249 + .../clubsite/ladda-themeless.min.css | 7 + 99 files changed, 18902 insertions(+), 29 deletions(-) create mode 100755 app/assets/images/clubsite/bootstrap-colorpicker/alpha-horizontal.png create mode 100755 app/assets/images/clubsite/bootstrap-colorpicker/alpha.png create mode 100755 app/assets/images/clubsite/bootstrap-colorpicker/hue-horizontal.png create mode 100755 app/assets/images/clubsite/bootstrap-colorpicker/hue.png create mode 100755 app/assets/images/clubsite/bootstrap-colorpicker/saturation.png create mode 100644 app/assets/images/clubsite/fancybox/blank.gif create mode 100644 app/assets/images/clubsite/fancybox/fancybox_loading.gif create mode 100644 app/assets/images/clubsite/fancybox/fancybox_overlay.png create mode 100644 app/assets/images/clubsite/fancybox/fancybox_sprite.png create mode 100644 app/assets/images/clubsite/fonts/glyphicons-halflings-regular.eot create mode 100644 app/assets/images/clubsite/fonts/glyphicons-halflings-regular.svg create mode 100644 app/assets/images/clubsite/fonts/glyphicons-halflings-regular.ttf create mode 100644 app/assets/images/clubsite/fonts/glyphicons-halflings-regular.woff create mode 100644 app/assets/javascripts/clubsite.js create mode 100644 app/assets/javascripts/clubsite/wjr/binding.js create mode 100644 app/assets/javascripts/clubsite/wjr/calendar.js create mode 100644 app/assets/javascripts/clubsite/wjr/club.js create mode 100644 app/assets/javascripts/clubsite/wjr/clubsite.js create mode 100644 app/assets/javascripts/clubsite/wjr/edit-event-courses.js create mode 100644 app/assets/javascripts/clubsite/wjr/edit-event-organizers.js create mode 100644 app/assets/javascripts/clubsite/wjr/editable.js create mode 100644 app/assets/javascripts/clubsite/wjr/event-list.js create mode 100644 app/assets/javascripts/clubsite/wjr/flickr-photos.js create mode 100644 app/assets/javascripts/clubsite/wjr/forms.js create mode 100644 app/assets/javascripts/clubsite/wjr/iof.js create mode 100644 app/assets/javascripts/clubsite/wjr/map.js create mode 100644 app/assets/javascripts/clubsite/wjr/register-others.js create mode 100644 app/assets/javascripts/clubsite/wjr/result-editor.js create mode 100644 app/assets/javascripts/clubsite/wjr/result-list.js create mode 100644 app/assets/javascripts/clubsite/wjr/utils.js create mode 100644 app/assets/javascripts/clubsite/wjr/wjr.js create mode 100644 app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js create mode 100644 app/assets/stylesheets/clubsite.css create mode 100644 app/assets/stylesheets/clubsite/embed.css create mode 100644 app/assets/stylesheets/clubsite/other.css create mode 100644 app/assets/stylesheets/clubsite/printable.css create mode 100644 app/assets/stylesheets/clubsite/whyjustrun.css create mode 100644 app/assets/stylesheets/clubsite/wysiwyg.css create mode 100644 app/assets/stylesheets/clubsite_embed.css create mode 100644 app/assets/stylesheets/clubsite_other.css create mode 100644 app/assets/stylesheets/clubsite_printable.css create mode 100644 app/constraints/club_domain_constraint.rb create mode 100644 app/controllers/clubsite/base_controller.rb create mode 100644 app/controllers/clubsite/errors_controller.rb create mode 100644 app/controllers/clubsite/pages_controller.rb create mode 100644 app/controllers/clubsite/robots_controller.rb create mode 100644 app/helpers/clubsite/layout_helper.rb create mode 100644 app/helpers/clubsite/menu_helper.rb create mode 100644 app/models/current.rb create mode 100644 app/views/clubsite/pages/home.html.erb create mode 100644 app/views/layouts/clubsite/_flash.html.erb create mode 100644 app/views/layouts/clubsite/_head.html.erb create mode 100644 app/views/layouts/clubsite/_series_css.html.erb create mode 100644 app/views/layouts/clubsite/default.html.erb create mode 100644 app/views/layouts/clubsite/embed.html.erb create mode 100644 app/views/layouts/clubsite/other.html.erb create mode 100644 app/views/layouts/clubsite/printable.html.erb delete mode 100644 public/robots.txt create mode 100644 test/integration/clubsite_tenancy_test.rb create mode 100644 test/integration/robots_test.rb create mode 100755 vendor/assets/javascripts/clubsite/bootstrap-colorpicker.js create mode 100644 vendor/assets/javascripts/clubsite/bootstrap-datetimepicker.js create mode 100644 vendor/assets/javascripts/clubsite/bootstrap-typeahead.js create mode 100644 vendor/assets/javascripts/clubsite/bootstrap.js create mode 100644 vendor/assets/javascripts/clubsite/fullcalendar.js create mode 100644 vendor/assets/javascripts/clubsite/jquery.fancybox.js create mode 100644 vendor/assets/javascripts/clubsite/jquery.jeditable.js create mode 100644 vendor/assets/javascripts/clubsite/jquery.js create mode 100644 vendor/assets/javascripts/clubsite/jquery.ketchup.js create mode 100644 vendor/assets/javascripts/clubsite/jquery.placeholder.js create mode 100644 vendor/assets/javascripts/clubsite/ketchup-bootstrap.js create mode 100644 vendor/assets/javascripts/clubsite/knockout.js create mode 100755 vendor/assets/javascripts/clubsite/ladda.js create mode 100644 vendor/assets/javascripts/clubsite/markerclusterer.js create mode 100644 vendor/assets/javascripts/clubsite/moment.js create mode 100644 vendor/assets/javascripts/clubsite/spin.js create mode 100644 vendor/assets/javascripts/clubsite/underscore.js create mode 100755 vendor/assets/stylesheets/clubsite/bootstrap-colorpicker.css.erb create mode 100644 vendor/assets/stylesheets/clubsite/bootstrap-datetimepicker.min.css create mode 100644 vendor/assets/stylesheets/clubsite/bootstrap.min.css.erb create mode 100644 vendor/assets/stylesheets/clubsite/fullcalendar.css create mode 100755 vendor/assets/stylesheets/clubsite/jquery.fancybox.css.erb create mode 100755 vendor/assets/stylesheets/clubsite/ladda-themeless.min.css diff --git a/.env.core.sample b/.env.core.sample index 7f5dd74..439c843 100644 --- a/.env.core.sample +++ b/.env.core.sample @@ -1,6 +1,9 @@ RAILS_ENV=development RAILS_LOG_TO_STDOUT=1 WJR_HOST=localhost:3000 +WJR_CORE_URL=http://localhost:3000 +WJR_GOOGLE_MAPS_API_KEY= +WJR_FLICKR_API_KEY= WJR_PASSWORD_SALT= WJR_SECRET_TOKEN=secret-token WJR_SECRET_BASE=secret-base diff --git a/app/assets/images/clubsite/bootstrap-colorpicker/alpha-horizontal.png b/app/assets/images/clubsite/bootstrap-colorpicker/alpha-horizontal.png new file mode 100755 index 0000000000000000000000000000000000000000..d0a65c08b0ed34d969cb0e93165a2e8b0ef9dbd7 GIT binary patch literal 3635 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 FV1h0U4od(4 literal 0 HcmV?d00001 diff --git a/app/assets/images/clubsite/bootstrap-colorpicker/alpha.png b/app/assets/images/clubsite/bootstrap-colorpicker/alpha.png new file mode 100755 index 0000000000000000000000000000000000000000..38043f1c85f20fe8ce37ad380bebe2d42d14fb29 GIT binary patch literal 3271 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/clubsite/bootstrap-colorpicker/hue.png b/app/assets/images/clubsite/bootstrap-colorpicker/hue.png new file mode 100755 index 0000000000000000000000000000000000000000..d89560e999f86c349ddc78c298b0328293f13e1e GIT binary patch literal 2972 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 >YjDHRkjMe3FzWUPlrRR$Vj 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/clubsite/fancybox/blank.gif b/app/assets/images/clubsite/fancybox/blank.gif new file mode 100644 index 0000000000000000000000000000000000000000..35d42e808f0a8017b8d52a06be2f8fec0b466a66 GIT binary patch literal 43 scmZ?wbhEHbWMp7uXkcLY|NlP&1B2pE7Dgb&paUX6G7L;iE{qJ;0LZEa`2YX_ literal 0 HcmV?d00001 diff --git a/app/assets/images/clubsite/fancybox/fancybox_loading.gif b/app/assets/images/clubsite/fancybox/fancybox_loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..01586176d793c261db6b6ca4ca7822b2d3b35703 GIT binary patch literal 3866 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.ttf b/app/assets/images/clubsite/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a498ef4e7c8b556fc36f580c5ff524025bb11c84 GIT binary patch literal 41236 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'); + if (settings.rows) { + textarea.attr('rows', settings.rows); + } else { + textarea.height(settings.height); + } + if (settings.cols) { + textarea.attr('cols', settings.cols); + } else { + textarea.width(settings.width); + } + $(this).append(textarea); + return textarea; + }, + submit: function (settings, original) { + WJR.wysiwyg.updateTextareas(); + }, + plugin: function (settings, original) { + // Hacktastic. This fixes an issue with the scroll position jumping on chrome when opening the textarea.. + var textarea = $('textarea', this); + setTimeout(function () { + WJR.wysiwyg.createRichTextArea(textarea); + }, 1); + } + }); + /*jslint unparam: false*/ + + // Allow link clicks to pass through without opening editor. NOTE: doesn't fix the case when the content is edited.. seems like it would be really hacky to get that working because jeditable doesn't have enough callback hooks. + editableContent.find('a').click(function (e) { + e.stopPropagation(); + }); + + $('.content-block.wjr-editable').editable('/contentBlocks/edit', { + type : 'wysiwyg', + cancel : 'Cancel', + submit : 'Save', + tooltip : 'Click to edit…', + onblur : 'ignore' + }); + + $('.page-resource.wjr-editable').editable('/pages/edit', { + type : 'wysiwyg', + cancel : 'Cancel', + submit : 'Save', + tooltip : 'Click to edit…', + onblur : 'ignore' + }); + + $('.page-resource-title.wjr-editable').editable('/pages/edit', { + type : 'text', + cancel : 'Cancel', + submit : 'Save', + name : 'name', + tooltip : 'Click to edit…', + onblur : 'ignore' + }); + }()); + } + }; + + return editable; +}(jQuery)); diff --git a/app/assets/javascripts/clubsite/wjr/event-list.js b/app/assets/javascripts/clubsite/wjr/event-list.js new file mode 100644 index 0000000..3ae472c --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/event-list.js @@ -0,0 +1,244 @@ +/*jslint nomen: true, browser: true, indent: 2*/ +/*global Ladda*/ + +window.WJR = window.WJR || {}; +WJR['event-list'] = (function ($, moment, ko, club) { + 'use strict'; + var EventList, SingleFetcher, TimeFetcher, IOF; + SingleFetcher = function (url, coloringStyle) { + this.fetch = function (options) { + $.ajax({ + type: "GET", + url: url, + dataType: "xml", + cache: false, + success: function (xml) { + options.onSuccess(IOF.loadEventsList(xml, coloringStyle)); + } + }); + }; + }; + + // A fetcher that fetches a time interval of events + TimeFetcher = function (url, coloringStyle) { + var startTime = null, endTime = null; + + /** + * Params: options + * - direction ('newer', or 'older') + * - onSuccess (callback, params: newEvents, startTime, endTime, newer) + * - onComplete (callback, called regardless of success or failure) + */ + this.fetch = function (options) { + var fetchingNewer, + fetchStartTime, + fetchEndTime; + + if (startTime === null || endTime === null) { + // Initial fetch + fetchStartTime = moment.utc().subtract('months', 1); + fetchEndTime = moment.utc().add('months', 2); + } else if (options.direction === 'newer') { + fetchStartTime = endTime; + fetchEndTime = endTime.clone().add('months', 6); + } else if (options.direction === 'older') { + fetchStartTime = startTime.clone().subtract('months', 6); + fetchEndTime = startTime; + } + + if (startTime === null || fetchStartTime.isBefore(startTime)) { + startTime = fetchStartTime; + } + + fetchingNewer = false; + if (endTime === null || fetchEndTime.isAfter(endTime)) { + fetchingNewer = true; + endTime = fetchEndTime; + } + + $.ajax({ + type: 'GET', + url: url, + data: { + start: fetchStartTime.unix(), + end: fetchEndTime.unix() + }, + dataType: 'xml', + cache: false, + success: function (xml) { + var events = IOF.loadEventsList(xml, coloringStyle); + options.onSuccess(events, startTime, endTime, fetchingNewer); + }, + complete: function () { + if (options.onComplete !== undefined) { + options.onComplete(); + } + } + }); + }; + }; + + EventList = function () { + var fetcher, + viewModel = { + startTime : ko.observable(), + endTime : ko.observable(), + events : ko.observableArray() + }; + + viewModel.formattedStartTime = ko.computed(function () { + var hasStartTime = (this.startTime() === null); + return hasStartTime ? this.startTime().format('MMMM Do YYYY') : null; + }, viewModel); + + viewModel.formattedEndTime = ko.computed(function () { + var hasEndTime = (this.endTime() === null); + return hasEndTime ? this.endTime().format('MMMM Do YYYY') : null; + }, viewModel); + + // Assumes the DOM is ready + this.initialize = function (element) { + var hasTimeWindow = false, + url, + olderButton, + newerButton, + successHandler, + coloringStyle = "default"; + if (element.getAttribute("data-event-list-type") === 'time-window') { + hasTimeWindow = true; + } + + if (element.hasAttribute("data-event-list-coloring")) { + coloringStyle = element.getAttribute("data-event-list-coloring"); + } + + url = element.getAttribute("data-event-list-url"); + ko.applyBindings(viewModel, element); + + if (hasTimeWindow) { + fetcher = new TimeFetcher(url, coloringStyle); + successHandler = function (newEvents, newStartTime, newEndTime, newer) { + var index = newer ? viewModel.events().length : 0; + // This adds the new events to the view model. + viewModel.events.splice.apply(viewModel.events, [index, 0].concat(newEvents)); + viewModel.startTime(newStartTime); + viewModel.endTime(newEndTime); + }; + + // Fetch the initial events + fetcher.fetch({ onSuccess: successHandler }); + + (function (Ladda) { + // Set up fetching when the buttons are pressed + olderButton = $(element.getAttribute('data-event-list-older-button')); + olderButton.click(function () { + var ladda = Ladda.create(olderButton.get(0)).start(); + fetcher.fetch({ + direction: 'older', + onSuccess: successHandler, + onComplete: function () { + ladda.stop(); + } + }); + }); + + newerButton = $(element.getAttribute('data-event-list-newer-button')); + newerButton.click(function () { + var ladda = Ladda.create(newerButton.get(0)).start(); + fetcher.fetch({ + direction: 'newer', + onSuccess: successHandler, + onComplete: function () { + ladda.stop(); + } + }); + }); + }(window.Ladda)); + } else { + fetcher = new SingleFetcher(url, coloringStyle); + fetcher.fetch({ + onSuccess: function (events) { + viewModel.events(events); + } + }); + } + }; + }; + + IOF = {}; + IOF.Event = function (id, name, url, startTime, endTime, classification, series, club) { + var ymd = 'MMMM Do YYYY', + md = 'MMMM Do', + sep = ' - '; + this.id = id; + this.name = name; + this.startTime = startTime; + this.endTime = endTime; + this.classification = classification; + this.series = series; + this.url = url; + this.clubAcronym = club.acronym; + if (!startTime.isSame(endTime, 'day')) { + if (!startTime.isSame(endTime, 'month')) { + if (!startTime.isSame(endTime, 'year')) { + this.date = startTime.format(ymd) + sep + endTime.format(ymd); + } else { + this.date = startTime.format(md) + sep + endTime.format(ymd); + } + } else { + this.date = startTime.format(md) + sep + endTime.format('Do, YYYY'); + } + } else { + this.date = startTime.format(ymd); + } + }; + + IOF.loadEventsList = function (xml, coloringStyle) { + var events = []; + /*jslint unparam: true*/ + $(xml.documentElement).children("Event").each(function (index, element) { + var eventID = $(element).children("Id").text(), + eventName = $(element).children("Name").text(), + extensions = $(element).children("Extensions"), + series = extensions.children('Series'), + color, + startTimeDate = $(element).children("StartTime"), + startDate = startTimeDate.children("ISODate").text(), + endTimeDate = $(element).children("EndTime"), + endDate = endTimeDate.children("ISODate").text(), + url = $(element).children('URL').text(), + organizerElement = $(element).children('Organiser'), + clubAcronym = organizerElement.children('ShortName').text(), + clubId = organizerElement.children('Id').text(), + classification = $(element).children('Classification').text(), + event; + // Need to get dates to be of format: 2011-10-10T14:48:00.000z + + if (coloringStyle === "club-only") { + if (clubId == club.id) { + color = series.children('Color').text() ? series.children('Color').text() : '#000000'; + } else { + color = '#6d6d6d'; + } + } else { + color = series.children('Color').text(); + } + + event = new IOF.Event( + eventID, + eventName, + url, + moment(startDate), + moment(endDate), + classification, + { color: color }, + { acronym: clubAcronym } + ); + events.push(event); + }); + /*jslint unparam: false*/ + return events; + }; + + return EventList; +}(jQuery, moment, ko, WJR.club)); diff --git a/app/assets/javascripts/clubsite/wjr/flickr-photos.js b/app/assets/javascripts/clubsite/wjr/flickr-photos.js new file mode 100644 index 0000000..bb71e18 --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/flickr-photos.js @@ -0,0 +1,67 @@ +/*jslint nomen: true, browser: true indent: 2*/ + +window.WJR = window.WJR || {}; +WJR['flickr-photos'] = (function ($, _, ko) { + 'use strict'; + var FlickrPhotos = {}, photoViewModel; + + photoViewModel = { + photos: ko.observableArray() + }; + + + FlickrPhotos.Initialize = function (element) { + var count = 2, + apiKey = element.getAttribute('data-flickr-api-key'), + tags = element.getAttribute('data-flickr-tags'); + + function loadMorePhotos(page) { + var url = "https://api.flickr.com/services/rest/?method=flickr.photos.search&nojsoncallback=1&api_key=" + apiKey + "&tags=" + tags + "&format=json&extras=date_taken,description,owner_name&per_page=30&page=" + page; + $.get(url, function (results) { + if (results.stat === "ok") { + _.each(results.photos.photo, function (photo) { + photoViewModel.photos.push({ + id: photo.id, + page: "https://www.flickr.com/photos/" + photo.owner + "/" + photo.id, + thumbnailUrl: "https://farm" + photo.farm + ".staticflickr.com/" + photo.server + "/" + photo.id + "_" + photo.secret + ".jpg", + largeUrl: "https://farm" + photo.farm + ".staticflickr.com/" + photo.server + "/" + photo.id + "_" + photo.secret + "_b.jpg", + ownerName: photo.ownername, + description: photo.description, + dateTaken: photo.datetaken, + clicked: function () { + var id = this.id, + largeUrl = this.largeUrl; + (function () { + if ($("#flickrPhoto" + id + " img").attr('src') === "") { + $("#flickrPhoto" + id + " img").attr('src', largeUrl); + $.fancybox.showLoading(); + + $("#flickrPhoto" + id + " img").load(function () { + $.fancybox.hideLoading(); + $.fancybox.open("#flickrPhoto" + id); + }); + } else { + $.fancybox.open("#flickrPhoto" + id); + } + }()); + } + }); + }); + } + }); + + } + + $(window).scroll(function () { + if ($(window).scrollTop() === $(document).height() - $(window).height()) { + loadMorePhotos(count); + count += 1; + } + }); + + loadMorePhotos(1); + ko.applyBindings(photoViewModel, element); + }; + + return FlickrPhotos; +}(jQuery, _, ko)); diff --git a/app/assets/javascripts/clubsite/wjr/forms.js b/app/assets/javascripts/clubsite/wjr/forms.js new file mode 100644 index 0000000..7ded94d --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/forms.js @@ -0,0 +1,103 @@ +/*jslint nomen: true, browser: true indent: 2*/ + +window.WJR = window.WJR || {}; +WJR.forms = (function ($, _) { + "use strict"; + var forms = {}; + + forms.SimplePersonPicker = function () { + this.initialize = function (element) { + var options = {}, callback, targetElement, configs; + targetElement = $(element.getAttribute('data-user-id-target')); + + configs = [['data-maintain-input', 'maintainInput'], + ['data-allow-fake', 'allowFake'], + ['data-create-new', 'createNew'], + ['data-allow-new', 'allowNew']]; + + _.each(configs, function (config) { + var dataAttribute = config[0], option = config[1], setting; + if (element.hasAttribute(dataAttribute)) { + setting = element.getAttribute(dataAttribute); + options[option] = (setting === 'true') ? true : false; + } + }); + + callback = function (person) { + // REVIEW: Should we set val to null when person is null? + if (person !== null) { + targetElement.val(person.id); + } + }; + + forms.personPicker(element, options, callback); + }; + }; + + // Callback should take a person object with id, name. Callback can also be called with null (no person selected) + // Maintain input will keep the selected user's name in the input after the input loses focus. "allowNew" will keep the input populated even if there is no user in the system with the given name. "createNew" will add an option to create a new user with the given name. + forms.personPicker = function (element, options, callback) { + (function () { + var displayName = null, + defaults = { + 'allowNew': false, + 'allowFake': true, + 'createNew': false + }; + options = options || {}; + options = $.extend({}, defaults, options); + + $(element).typeahead({ + source: function (typeahead, query) { + $.ajax({ + url: "/users/index.json?term=" + query + "&allowFake=" + (options.allowFake ? 'true' : 'false'), + success: function (data) { + if (options.createNew) { + data.push({ + position: "bottom", + name: query, + identifiableName: 'Create New User (' + query + ')' + }); + } + typeahead.process(data); + } + }); + }, + onselect: function (person) { + callback(person); + if (options.maintainInput) { + displayName = person.name; + $(element).val(displayName); + } + $(element).blur(); + }, + property: "identifiableName" + }); + + $(element).blur(function () { + if (!options.allowNew) { + if ($(element).val() === "") { + callback(null); + } else if ($(element).val() !== displayName) { + $(element).val(displayName); + } + } + }); + }()); + }; + + forms.validation = {}; + forms.validation.checkKetchupFormsAreValidOnSubmit = function () { + $('[data-validate="ketchup"]').each(function () { + (function () { + var jqe = $(this); + jqe.submit(function () { + jqe.ketchup(); + return jqe.ketchup('isValid'); + }); + }()); + }); + }; + + return forms; +}(jQuery, _)); diff --git a/app/assets/javascripts/clubsite/wjr/iof.js b/app/assets/javascripts/clubsite/wjr/iof.js new file mode 100644 index 0000000..76eaf12 --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/iof.js @@ -0,0 +1,142 @@ +/*jslint nomen: true, browser: true indent: 2*/ + +window.WJR = window.WJR || {}; +WJR.iof = (function ($, _, ko, moment, utils) { + 'use strict'; + var IOF = {}; + IOF.ResultList = function (event, creationTime) { + this.event = ko.observable(event); + this.creationTime = ko.observable(creationTime); + this.formattedCreationTime = ko.computed(function () { + var time = this.creationTime(); + return time ? time.format("dddd, MMMM Do YYYY [at] h:mm:ss a") : null; + }, this); + }; + + IOF.Event = function (id, name, startTime, courses) { + this.id = id; + this.name = name; + this.startTime = startTime; + this.courses = courses; + }; + + IOF.Course = function (id, name, results, scoringType, millisecondTiming) { + this.id = id; + this.name = name; + this.results = ko.observableArray(results); + this.hasComments = ko.computed(function () { + return _.some(this.results(), function (result) { + return (result.officialComment !== ""); + }); + }, this); + this.hasWhyJustRunPoints = ko.computed(function () { + return _.some(this.results(), function (result) { + return (result.scores.WhyJustRun !== undefined); + }); + }, this); + this.scoringType = scoringType; + this.isScore = (scoringType === 'Points'); + this.isTimed = (scoringType === 'Timed'); + this.millisecondTiming = millisecondTiming; + }; + + IOF.friendlyStatuses = { + 'OK': '', + 'Inactive': 'Inactive', + 'DidNotStart': 'DNS', + 'Active': 'In Progress', + 'Finished': 'Unofficial', + 'MissingPunch': 'MP', + 'DidNotFinish': 'DNF', + 'DidNotEnter': 'DNE', + 'Disqualified': 'DSQ', + 'NotCompeting': 'NC', + 'SportWithdrawal': 'Sport Withdrawal', + 'OverTime': 'Over Time', + 'Moved': 'Moved', + 'MovedUp': 'Moved Up', + 'Cancelled': 'Cancelled' + }; + + IOF.Result = function (time, position, status, scores, officialComment, person) { + if (time !== null && !isNaN(time)) { + this.time = time; + this.hours = utils.zeroFill((time - time % 3600) / 3600, 2); + this.minutes = utils.zeroFill((time - time % 60) / 60 - this.hours * 60, 2); + var millis = Math.round((time % 1) * 1000); + this.seconds = utils.zeroFill(Math.floor(time) % 60, 2); + this.milliseconds = utils.zeroFill(millis, 3); + } else { + this.time = this.hours = this.minutes = this.seconds = this.milliseconds = null; + } + + this.status = status; + this.friendlyStatus = IOF.friendlyStatuses[status]; + this.position = position; + this.scores = scores; + this.person = person; + this.officialComment = officialComment; + }; + + IOF.Person = function (id, givenName, familyName, profileUrl) { + this.id = id; + this.givenName = givenName; + this.familyName = familyName; + this.profileUrl = profileUrl; + }; + + IOF.loadResultsList = function (xml) { + var resultList, date, event, courses, iofEvent; + + resultList = $(xml.documentElement); + date = moment(resultList.attr('createTime')); + event = resultList.children("Event").first(); + courses = []; + resultList.children('ClassResult').each(function () { + var element, classElement, courseId, courseName, courseExtensions, + scoringType, results, millisecondTiming; + element = $(this); + classElement = element.children('Class'); + courseId = classElement.children("Id").text(); + courseName = classElement.children('Name').text(); + courseExtensions = element.children("Course").children('Extensions'); + scoringType = courseExtensions.children('ScoringType').text(); + results = []; + millisecondTiming = false; + element.children('PersonResult').each(function () { + var prElement = $(this), person, personGivenName, personFamilyName, + personProfileUrl, personId, resultElement, resultTime, + resultStatus, resultExtensions, officialComment, resultPosition, + resultScores, result; + person = prElement.children("Person").first(); + personGivenName = person.children("Name").children("Given").text(); + personFamilyName = person.children("Name").children("Family").text(); + personProfileUrl = person.children("Contact[type='WebAddress']").text(); + personId = person.children("Id").text(); + resultElement = prElement.children("Result"); + resultTime = parseFloat(resultElement.children("Time").text()); + resultStatus = resultElement.children("Status").text(); + resultExtensions = resultElement.children("Extensions"); + officialComment = resultExtensions.children("OfficialComment").text(); + resultPosition = resultElement.children("Position").text(); + resultScores = {}; + prElement.children("Result").children("Score").each(function () { + resultScores[$(this).attr("type")] = $(this).text(); + }); + result = new IOF.Result(resultTime, resultPosition, resultStatus, resultScores, officialComment, new IOF.Person(personId, personGivenName, personFamilyName, personProfileUrl)); + if (!millisecondTiming && + result.milliseconds !== '000' && + result.milliseconds !== null) { + millisecondTiming = true; + } + results.push(result); + }); + courses.push(new IOF.Course(courseId, courseName, results, scoringType, millisecondTiming)); + }); + + iofEvent = new IOF.Event(event.children("Id").text(), event.children("Name").text(), null, courses); + return new IOF.ResultList(iofEvent, date); + }; + + return IOF; +}(jQuery, _, ko, moment, WJR.utils)); diff --git a/app/assets/javascripts/clubsite/wjr/map.js b/app/assets/javascripts/clubsite/wjr/map.js new file mode 100644 index 0000000..c5f0d3d --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/map.js @@ -0,0 +1,184 @@ +/*jslint browser: true, indent: 2, nomen: true*/ +/*global google*/ + +window.WJR = window.WJR || {}; +WJR.map = (function ($, _) { + 'use strict'; + var map = {}, googleMapsPromise = null, loadGoogleMaps; + + // Loads the Google Maps API on first use. The API key is provided by the + // page via a meta tag, and the returned promise resolves once the API's + // callback= parameter fires. + loadGoogleMaps = function () { + if (googleMapsPromise === null) { + googleMapsPromise = new Promise(function (resolve) { + var key = document.querySelector('meta[name="wjr.google-maps-key"]').content, + script = document.createElement('script'); + window.wjrGoogleMapsLoaded = resolve; + script.src = 'https://maps.googleapis.com/maps/api/js?key=' + encodeURIComponent(key) + '&callback=wjrGoogleMapsLoaded'; + document.head.appendChild(script); + }); + } + return googleMapsPromise; + }; + + map.SimpleMarkerMap = function () { + this.initialize = function (element) { + loadGoogleMaps().then(function () { + var lat, lng, zoom, mobileUrl, modalEvents, + mapOptions, marker, center, googleMap, clickHandler, useMobileUrl; + lat = +(element.getAttribute('data-lat')); + lng = +(element.getAttribute('data-lng')); + zoom = +(element.getAttribute('data-zoom')); + mobileUrl = element.getAttribute('data-mobile-url'); + useMobileUrl = ($(window).width() < 768) && mobileUrl !== null; + center = new google.maps.LatLng(lat, lng); + mapOptions = { + zoom: zoom, + center: center, + scrollwheel: false, + draggable: !useMobileUrl, + disableDefaultUI: useMobileUrl, + mapTypeId: google.maps.MapTypeId.HYBRID + }; + + googleMap = new google.maps.Map(element, mapOptions); + + marker = new google.maps.Marker({ + position: center + }); + marker.setMap(googleMap); + + if (useMobileUrl) { + modalEvents = ['click']; + clickHandler = function () { + window.location = mobileUrl; + }; + + _.each(modalEvents, function (modalEvent) { + google.maps.event.addListener(googleMap, modalEvent, clickHandler); + }); + } + }); + }; + }; + + map.DraggableMarkerMap = function () { + this.initialize = function (element) { + loadGoogleMaps().then(function () { + var latElement, lngElement, zoom, googleMap, options, center, marker; + latElement = $(element.getAttribute('data-lat-element')); + lngElement = $(element.getAttribute('data-lng-element')); + zoom = +(element.getAttribute('data-zoom')); + + center = new google.maps.LatLng(latElement.val(), lngElement.val()); + options = { + center: center, + zoom: zoom, + mapTypeId: google.maps.MapTypeId.HYBRID + }; + googleMap = new google.maps.Map(element, options); + + marker = new google.maps.Marker({ + position: center, + draggable: true + }); + + google.maps.event.addListener(marker, 'dragend', function () { + var position = marker.getPosition(); + latElement.val(position.lat()); + lngElement.val(position.lng()); + }); + + marker.setMap(googleMap); + }); + }; + }; + + map.MultiMarkerMap = function () { + this.initialize = function (element) { + loadGoogleMaps().then(function () { + var lat, fetchUrl, fetchEntity, fetchMarkers, fetchMarkerImages, fetchBounds, + lng, zoom, googleMap, markerClusterer, options, center, markers, fetchInProgress; + lat = +(element.getAttribute('data-lat')); + lng = +(element.getAttribute('data-lng')); + zoom = +(element.getAttribute('data-zoom')); + fetchUrl = element.getAttribute('data-fetch-url'); + fetchEntity = element.getAttribute('data-fetch-entity'); + fetchMarkerImages = element.getAttribute('data-fetch-markerimages'); + center = new google.maps.LatLng(lat, lng); + options = { + center: center, + zoom: zoom, + mapTypeId: google.maps.MapTypeId.HYBRID + }; + + googleMap = new google.maps.Map(element, options); + markers = {}; + markerClusterer = new MarkerClusterer(googleMap, markers, + {imagePath: fetchMarkerImages, maxZoom: 11}); + + fetchInProgress = false; + fetchBounds = null; + fetchMarkers = function () { + if (!fetchInProgress) { + fetchInProgress = true; + var bounds, ne, sw, url; + bounds = googleMap.getBounds(); + fetchBounds = bounds; + ne = bounds.getNorthEast(); + sw = bounds.getSouthWest(); + url = fetchUrl; + url += '?max_lat=' + ne.lat() + '&min_lat=' + sw.lat(); + url += '&max_lng=' + ne.lng() + '&min_lng=' + sw.lng(); + $.ajax({ + url: url, + success: function (data) { + _.each(data[fetchEntity], function (entity) { + if (_.has(markers, entity.id)) { + return; + } + var marker, color, infoWindow; + infoWindow = new google.maps.InfoWindow({ + content: '

' + entity.name + '

' + }); + color = (entity.map_standard && entity.map_standard.color) ? entity.map_standard.color : 'rgba(0,0,0,1)'; + marker = new google.maps.Marker({ + position: new google.maps.LatLng(entity.lat, entity.lng), + icon: { + path: "M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z", + fillColor: color, + fillOpacity: 1.0, + anchor: new google.maps.Point(12, 24), + strokeOpacity: 0.5, + strokeWeight: 2.0, + strokeColor: '#000000', + scale: 2.0 + }, + }); + google.maps.event.addListener(marker, 'click', function () { + infoWindow.open(googleMap, marker); + }); + markerClusterer.addMarker(marker); + markers[entity.id] = marker; + }); + }, + complete: function () { + fetchInProgress = false; + // If the bounds have changed since the last fetch, start another fetch + if (!fetchBounds.equals(googleMap.getBounds())) { + fetchMarkers(); + } + } + }); + } + }; + + google.maps.event.addListener(googleMap, 'bounds_changed', fetchMarkers); + }); + }; + }; + + return map; +}(jQuery, _)); diff --git a/app/assets/javascripts/clubsite/wjr/register-others.js b/app/assets/javascripts/clubsite/wjr/register-others.js new file mode 100644 index 0000000..1af7940 --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/register-others.js @@ -0,0 +1,48 @@ +/*jslint browser: true, indent: 2*/ +/*global confirm, alert*/ + +window.WJR = window.WJR || {}; +WJR['register-others'] = (function ($, forms) { + 'use strict'; + return function () { + this.initialize = function (element) { + element = $(element); + element.find('#RegisterOthersUserId').val(null); + + var options = { maintainInput: true, allowNew: true }; + forms.personPicker(element.find('#RegisterOthersUserName'), options, function (person) { + element.find('#RegisterOthersUserId').val((person !== null) ? person.id : null); + }); + + function completeSubmit(courseId, userId) { + location.href = "/courses/register/" + courseId + "/" + userId; + } + + element.find('#RegisterOthersSubmit').click(function () { + var userId, courseId, userName; + userId = element.find('#RegisterOthersUserId').val(); + courseId = element.find('#RegisterOthersCourse').val(); + if (!userId) { + userName = element.find('#RegisterOthersUserName').val(); + if (userName) { + if (userName.indexOf(" ") !== -1) { + if (confirm("This registration will create a new user in the system. Are you sure " + userName + " isn't already an WhyJustRun user?")) { + $.post('/users/add', { userName: userName }, function (data) { + completeSubmit(courseId, $.parseJSON(data)); + }); + } else { + alert("Thanks! Please re-enter the participant's name and choose the matching person from the dropdown."); + } + } else { + alert("Please enter the participant's full name."); + } + } else { + alert("Please enter the participant name"); + } + } else { + completeSubmit(courseId, userId); + } + }); + }; + }; +}(jQuery, WJR.forms)); diff --git a/app/assets/javascripts/clubsite/wjr/result-editor.js b/app/assets/javascripts/clubsite/wjr/result-editor.js new file mode 100644 index 0000000..074bfc3 --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/result-editor.js @@ -0,0 +1,178 @@ +/*jslint browser: true, indent: 2, nomen: true*/ +/*global confirm, alert*/ + +window.WJR = window.WJR || {}; +WJR['result-editor'] = (function ($, _, ko, utils, forms) { + 'use strict'; + return function () { + var _statuses = null, statuses, sortedStatuses, + ResultStatus, Course, Result, User, Event, + viewModel, loadObjects, eventId; + statuses = function () { + var map; + if (_statuses === null) { + // TODO: We shouldn't need this non-standard mapping after we start using IOF standard statuses.. + map = { + 'ok': 'Finished', + 'inactive': 'Inactive', + 'did_not_start': 'DNS', + 'active': 'In Progress', + 'finished': 'Unofficial', + 'mis_punch': 'MP', + 'did_not_enter': 'DNE', + 'did_not_finish': 'DNF', + 'disqualified': 'DSQ', + 'not_competing': 'NC', + 'sport_withdrawal': 'Sport Withdrawal', + 'over_time': 'Over Time', + 'moved': 'Moved', + 'moved_up': 'Moved Up', + 'cancelled': 'Cancelled' + }; + + _statuses = {}; + _.each(map, function (k, v) { + _statuses[k] = new ResultStatus(v, k); + }); + } + + return _statuses; + }; + + sortedStatuses = function () { + return _.values(statuses()).sort(function (a, b) { return a.name.localeCompare(b.name); }); + }; + + ResultStatus = function (id, name) { + this.id = id; + this.name = (name === null) ? statuses()[id] : name; + }; + + Course = function (id, name, distance, climb, event_id, description, results, is_score_o) { + this.id = id; + this.name = name; + this.distance = distance; + this.climb = climb; + this.event_id = event_id; + this.description = description; + this.results = ko.observableArray(results); + this.is_score_o = ko.observable(is_score_o); + this.rankBy = ko.computed({ + read: function () { + return this.is_score_o() ? "points" : "time"; + }, + write: function (value) { + this.is_score_o((value === "points")); + }, + owner: this + }); + }; + + Result = function (id, user, course_id, time_seconds, status, registrant_comment, official_comment, score_points) { + var hoursCount = Math.floor(time_seconds / 3600), + minutesCount = Math.floor((time_seconds - hoursCount * 3600) / 60), + secondsCount = time_seconds - hoursCount * 3600 - minutesCount * 60, + millisecondsCount = Math.round((secondsCount % 1) * 1000); + + this.id = id; + this.user = user; + this.course_id = course_id; + this.hours = ko.observable(time_seconds ? utils.zeroFill(hoursCount, 2) : '00'); + this.minutes = ko.observable(time_seconds ? utils.zeroFill(minutesCount, 2) : '00'); + this.seconds = ko.observable(time_seconds ? utils.zeroFill(Math.floor(secondsCount), 2) : '00'); + this.milliseconds = ko.observable(time_seconds ? utils.zeroFill(millisecondsCount, 3) : '000'); + this.statuses = sortedStatuses; + this.status = ko.observable(status || 'ok'); + this.registrant_comment = ko.observable(registrant_comment); + this.official_comment = ko.observable(official_comment); + this.score_points = ko.observable(score_points); + + this.remove = function () { + if (this.id) { + if (!confirm("Are you sure you want to delete this competitor? The entry be deleted from the server immediately.")) { + return; + } + $.ajax('/results/delete/' + this.id); + } + + var courses = viewModel.courses(), + i; + for (i = 0; i < viewModel.courses().length; i += 1) { + courses[i].results.remove(this); + } + }; + }; + + User = function (id, name) { + this.id = id; + this.name = name; + }; + + Event = function (id, name, date) { + this.id = id; + this.name = name; + this.date = date; + }; + + viewModel = { + courses : ko.observableArray(), + event : ko.observable(), + userName: ko.observable(), + selectedCourse: ko.observable(), + addCompetitorToCourse: function (id, name) { + var options, successHandler, user; + if (!viewModel.selectedCourse()) { + alert("Adding competitor failed, because a course wasn't selected."); + return; + } + + if (id === undefined) { + // create user asynchronously, then add to the UI once we've go the user ID. + options = { + userName: name, + eventId: viewModel.event().id + }; + successHandler = function (userID) { + viewModel.addCompetitorToCourse($.parseJSON(userID), name); + }; + $.post("/users/add", options, successHandler); + } else { + user = new User(id, name); + viewModel.selectedCourse().results.push(new Result(undefined, user, viewModel.selectedCourse().id, undefined, undefined, undefined, undefined, undefined, undefined)); + } + } + }; + + loadObjects = function () { + // TODO: We should use the result list xml api. + $.getJSON('/events/view/' + eventId + '.json', function (data) { + viewModel.event(new Event(data.Event.id, data.Event.name, data.Event.date)); + var courses = data.Course; + _.each(courses, function (course) { + var results = course.Result, + importedResults = []; + + _.each(results, function (result) { + var user = new User(result.User.id, result.User.name); + importedResults.push(new Result(result.id, user, result.course_id, result.time_seconds, result.status, result.registrant_comment, result.official_comment, result.score_points)); + }); + + viewModel.courses.push(new Course(course.id, course.name, course.distance, course.climb, course.event_id, course.description, importedResults, course.is_score_o)); + }); + }); + }; + + this.initialize = function (element) { + eventId = element.getAttribute('data-event-id'); + loadObjects(); + ko.applyBindings(viewModel, element); + var options = { maintainInput: false, createNew: true }, + pickerElement = $('#competitorResults'); + forms.personPicker(pickerElement, options, function (person) { + if (person !== null) { + viewModel.addCompetitorToCourse(person.id, person.name); + } + }); + }; + }; +}(jQuery, _, ko, WJR.utils, WJR.forms)); diff --git a/app/assets/javascripts/clubsite/wjr/result-list.js b/app/assets/javascripts/clubsite/wjr/result-list.js new file mode 100644 index 0000000..b8ea254 --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/result-list.js @@ -0,0 +1,43 @@ +/*jslint browser: true indent: 2*/ + +window.WJR = window.WJR || {}; +WJR['result-list'] = (function ($, IOF, ko) { + 'use strict'; + return function () { + var viewModel, fetchResults, url; + + viewModel = { + resultList: ko.observable() + }; + + fetchResults = function () { + $.ajax({ + type: "GET", + url: url, + dataType: "xml", + ifModified: true, + success: function (xml) { + var resultList = IOF.loadResultsList(xml); + viewModel.resultList(resultList); + } + }); + }; + + this.initialize = function (element) { + var modeAttr = 'data-result-list-mode', + mode = element.hasAttribute(modeAttr) ? element.getAttribute(modeAttr) : 'normal'; + + url = element.getAttribute("data-result-list-url"); + + fetchResults(); + + if (mode === 'live') { + window.setInterval(fetchResults, 5000); + } + + ko.applyBindings(viewModel, element); + }; + + return this; + }; +}(jQuery, WJR.iof, ko)); diff --git a/app/assets/javascripts/clubsite/wjr/utils.js b/app/assets/javascripts/clubsite/wjr/utils.js new file mode 100644 index 0000000..5294f36 --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/utils.js @@ -0,0 +1,16 @@ +/*jslint browser: true indent: 2*/ + +window.WJR = window.WJR || {}; +WJR.utils = (function () { + "use strict"; + var utils = {}; + utils.zeroFill = function (number, width) { + width -= number.toString().length; + if (width > 0) { + return new Array(width + (/\./.test(number) ? 2 : 1) ).join('0') + number; + } + return number; + }; + + return utils; +}()); diff --git a/app/assets/javascripts/clubsite/wjr/wjr.js b/app/assets/javascripts/clubsite/wjr/wjr.js new file mode 100644 index 0000000..f8db2b0 --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/wjr.js @@ -0,0 +1,22 @@ +/*jslint browser: true indent: 2*/ + +window.WJR = window.WJR || {}; +WJR.wjr = (function ($) { + 'use strict'; + var wjr = {}; + wjr.core = {}; + wjr.core.domain = $('meta[name="wjr.core.domain"]').attr("content"); + + $.ajaxSetup({ + beforeSend: function (xhr, settings) { + if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) { + var token = $('meta[name="csrf-token"]').attr('content'); + if (token) { + xhr.setRequestHeader('X-CSRF-Token', token); + } + } + } + }); + + return wjr; +}(jQuery)); diff --git a/app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js b/app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js new file mode 100644 index 0000000..7348328 --- /dev/null +++ b/app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js @@ -0,0 +1,20 @@ +/*jslint browser: true indent: 2*/ + +// NOTE: Requires the redactor library ($.fn.redactor), which is not part of +// this bundle (it was injected at deploy time in the original clubsite app). +window.WJR = window.WJR || {}; +WJR.wysiwyg = (function ($) { + 'use strict'; + var wysiwyg = {}; + wysiwyg.updateTextareas = function () { return; }; + wysiwyg.createRichTextArea = function (element) { + $(element).redactor({ + toolbarFixed: true, + toolbarFixedBox: true, + imageUpload: '/proxies/redactor/uploadImage', + fileUpload: '/proxies/redactor/uploadFile' + }); + }; + + return wysiwyg; +}(jQuery)); diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 42fe63d..6ff683b 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -3,5 +3,22 @@ * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * the top of the compiled file, but it's generally better to create a new file per style scope. *= require_self - *= require_tree . + *= require bootstrap_and_overrides + *= require clubs + *= require content_blocks + *= require courses + *= require events + *= require groups + *= require home + *= require map_standards + *= require maps + *= require memberships + *= require nav + *= require organizers + *= require pages + *= require privileges + *= require results + *= require roles + *= require series + *= require users */ diff --git a/app/assets/stylesheets/clubsite.css b/app/assets/stylesheets/clubsite.css new file mode 100644 index 0000000..5aecea2 --- /dev/null +++ b/app/assets/stylesheets/clubsite.css @@ -0,0 +1,12 @@ +/* + * Clubsite stylesheet bundle: bootstrap, plugin CSS (formerly loaded on + * demand via the RequireJS css! plugin), then the first-party styles. + * + *= require clubsite/bootstrap.min + *= require clubsite/fullcalendar + *= require clubsite/jquery.fancybox + *= require clubsite/ladda-themeless.min + *= require clubsite/bootstrap-datetimepicker.min + *= require clubsite/bootstrap-colorpicker + *= require clubsite/whyjustrun + */ diff --git a/app/assets/stylesheets/clubsite/embed.css b/app/assets/stylesheets/clubsite/embed.css new file mode 100644 index 0000000..f5bae2c --- /dev/null +++ b/app/assets/stylesheets/clubsite/embed.css @@ -0,0 +1,3 @@ +body { + background: none; +} diff --git a/app/assets/stylesheets/clubsite/other.css b/app/assets/stylesheets/clubsite/other.css new file mode 100644 index 0000000..7b21ca0 --- /dev/null +++ b/app/assets/stylesheets/clubsite/other.css @@ -0,0 +1,23 @@ +/* Need this for the floating menu bar */ +body { + padding-top: 60px; +} +@media (max-width: 979px) { + body { + padding-top: 0px; + } +} + +#content { + padding-left: 20px; + padding-bottom: 15px; + min-height: 0%; + padding-top: 0; + background-color: #fff; +} + +.page-header { + border-bottom: 1px solid #AAA; + margin: 0px; +} + diff --git a/app/assets/stylesheets/clubsite/printable.css b/app/assets/stylesheets/clubsite/printable.css new file mode 100644 index 0000000..d215a81 --- /dev/null +++ b/app/assets/stylesheets/clubsite/printable.css @@ -0,0 +1,54 @@ +body { + font-family: "Helvetica","Arial",sans-serif; + font-size: 80%; + margin: 0; + padding: 0; + text-align: left; +} + +/** Tables **/ +table { + padding: 2px; + margin: 0px; + clear: both; + color: #000; + width: 100%; + font-size: 15px; + border-spacing: 0px; + border: 2px solid #000; +} + +thead th { + font-weight: bold; + text-decoration: none; + border-left: 1px solid #000; + border-bottom: 2px solid #000; + border-top: 2px solid #000; + padding: 5px; +} + +/* +th a { + display: block; + padding: 2px 4px; + text-decoration: none; +} +*/ +table tr td { + text-align: left; + border-bottom: 1px solid #000; + border-left: 1px solid #000; + padding: 5px; + margin: 0px; +} +td { + background: #fff; +} +td.actions { + text-align: center; + white-space: nowrap; +} +table td.actions a { + margin: 0px 6px; + padding: 2px 5px; +} diff --git a/app/assets/stylesheets/clubsite/whyjustrun.css b/app/assets/stylesheets/clubsite/whyjustrun.css new file mode 100644 index 0000000..1c85bd5 --- /dev/null +++ b/app/assets/stylesheets/clubsite/whyjustrun.css @@ -0,0 +1,503 @@ +/** + * + * Generic CSS for CakePHP + * + * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) + * Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org) + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @copyright Copyright 2005-2010, Cake Software Foundation, Inc. (http://cakefoundation.org) + * @link http://cakephp.org CakePHP(tm) Project + * @package cake + * @subpackage cake.app.webroot.css + * @since CakePHP(tm) + * @license MIT License (http://www.opensource.org/licenses/mit-license.php) + */ + +/* Registration deadline additional classes */ +.reg-deadline-header { + color: #d2322d; + padding: 5px; +} + +/* Use to ensure the image fits within the responsive column*/ +.fitting-image { + max-width: 100%; +} + +/* inputs that are ketchup validated will not be aligned properly if there is a validation error. This works around that issue. */ +.form-group.inline-validated { + vertical-align: top; +} + +.inline-validated-help { + vertical-align: -webkit-baseline-middle; +} +/* Fix for nesting of inline form inside horizontal form (http://stackoverflow.com/questions/18429121/inline-form-nested-within-horizontal-form-in-bootstrap-3) */ +.form-inline .form-group { + margin-left: 0; + margin-right: 0; +} + +.ketchup-custom { + line-height: 1.5em; + padding: 0; + margin: 0; + margin-top: 5px; + display: none; +} + +.ketchup-custom li { + font-size: 12px; + text-transform: uppercase; + text-shadow: 1px 1px 0 #9F4631; + border: 0; + color: white; + background: #F46644; + padding: 1px 10px; + margin: 0; +} + +/** Placeholder polyfill **/ +input, textarea { + color: #000; +} +.placeholder { + color: #aaa; +} + +/** Facebook bugfix **/ +.fb-like-box iframe { + height: 100px; +} + +/** Thinner select input, useful for tables**/ +input.thin-control, select.thin-control, form.thin-form select, form.thin-form input { + height: 18px; + min-height: 18px; + padding: 0; + padding-left: 2px; + padding-right: 2px; + margin-bottom: 0; +} + +input.spanning-control { + width: 99%; +} + +form.thin-form { + margin: 0; +} + +/** Result ride sharing **/ +#ResultEditRideForm { + margin: 0; +} + +/** General Style Info **/ + +#content { + padding-bottom: 15px; +} + +/** Event page **/ + +.social-event-button { + padding: 7px; + padding-left: 10px; + padding-right: 10px; + margin-bottom: 15px; +} + +.social-event-text { + padding-left: 3px; + vertical-align: bottom; +} + +.event-header { + padding: 0; + margin: 0; +} + +.course-info h3 { + text-align: left; + padding-left: 0px; +} + +.column-box { + border-radius: 10px; + overflow: hidden; + margin: 10px; + padding: 10px; + background-color: #f8f8f8; +} + +.column-box h2 { + padding-top: 0px; +} + +#location-map { + height: 500px; +} + +.course-editing input { + font-size: 100%; +} + +.content-block img { + max-width: 100%; +} + +.content-block textarea { + font-size: 100%; +} + +.wjr-editable:hover { + background-color: #fffdd7; + cursor: pointer; + border-radius: 10px; +} + +.wjr-editable:hover > form { + cursor: initial; +} + +.wjr-editable .btn { + margin-right: 7px; +} + +/** +Results editing + **/ + +.results-editing input { + font-size: small; + +} + +.results-editing { + padding: 0; + width: 5%; + padding-left: 5px; +} + +/** Navigation bar styling **/ + +.navbar-constrained-width { + max-width: 100%; + width: 1200px; + margin:0 auto 0 auto; +} + +.navbar-squared-top { + border-top-left-radius: 0px; + border-top-right-radius: 0px; +} + +.navbar-nav-main-text { + font-weight: bold; + font-size: 16px; +} + +/* Custom navbar colouring style (generated from http://jsfiddle.net/drSbw/102/) */ +.navbar-colored { + background-color: #238216; + border-color: #1e6613; +} +.navbar-colored .navbar-brand { + color: #ecf0f1; +} +.navbar-colored .navbar-brand:hover, .navbar-colored .navbar-brand:focus { + color: white; +} +.navbar-colored .navbar-nav > li > a { + color: #ecf0f1; +} +.navbar-colored .navbar-nav > li > a:hover, .navbar-colored .navbar-nav > li > a:focus { + background-color: #238216; + color: white; +} +.navbar-colored .navbar-nav .active > a, .navbar-colored .navbar-nav .active > a:hover, .navbar-colored .navbar-nav .active > a:focus { + color: white; + background-color: #1e6613; +} +.navbar-colored .navbar-nav .open > a, .navbar-colored .navbar-nav .open > a:hover, .navbar-colored .navbar-nav .open > a:focus { + color: white; + background-color: #1e6613; +} +.navbar-colored .navbar-nav .open > a .caret, .navbar-colored .navbar-nav .open > a:hover .caret, .navbar-colored .navbar-nav .open > a:focus .caret { + border-top-color: white; + border-bottom-color: white; +} +.navbar-colored .navbar-nav > .dropdown > a .caret { + border-top-color: #ecf0f1; + border-bottom-color: #ecf0f1; +} +.navbar-colored .navbar-nav > .dropdown > a:hover .caret, .navbar-colored .navbar-nav > .dropdown > a:focus .caret { + border-top-color: white; + border-bottom-color: white; +} +.navbar-colored .navbar-toggle { + border-color: #1e6613; +} +.navbar-colored .navbar-toggle:hover, .navbar-colored .navbar-toggle:focus { + background-color: #1e6613; +} +.navbar-colored .navbar-toggle .icon-bar { + background-color: #ecf0f1; +} + +@media (max-width: 767px) { + .navbar-colored .navbar-nav .open .dropdown-menu > li > a { + color: #ecf0f1; + } + .navbar-colored .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-colored .navbar-nav .open .dropdown-menu > li > a:focus { + color: white; + background-color: #1e6613; + } +} + +header.header h1 { + text-align: center; + font-size: 40px; + line-height: 50px; +} + +/** Footer **/ + +#credits { + text-align: right; +} + +footer.other-footer, footer.main-footer, +footer.other-footer a, footer.other-footer a:hover, +footer.main-footer a, footer.main-footer a:hover { + color: #fff; +} + +footer.other-footer { + padding: 5px; + background-color: black; +} + +footer.main-footer, footer.other-footer { + color: #fff; +} + +footer.main-footer { + padding-bottom: 5px; + background-color: #213449; +} + +footer.main-footer div { + padding: 5px; + padding-left: 12px; +} + +/** General styling **/ + +.page-header > h1 { + text-align: left; +} + +/** Layout **/ + +.three-column { + /** Needed so three-column surrounds the enclosed divs **/ + overflow:hidden; +} + +.three-column h2, .three-column h3 { + text-align: center; +} + +.three-column .events-by-series h4 { + font-weight: bold; +} + +.event-box { + margin-bottom: 10px; + margin-top: 10px; +} + +/** Outer event-box is needed to safely override the a style and still allow for block clicking. **/ +.event-box a { + color: #000000; + text-decoration: none; + +} +.event-box-icon { + position: absolute; + right: 0; + top: 0; + height: 60px; +} + +.event-box-icon > img { + width: 60px; + height: 60px; + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; +} + +.event-box-inner { + position: relative; +} + +.event-box-inner { + background-color: #f8f8f8; + border-radius: 10px; + padding: 6px; + padding-left: 11px; + padding-right: 11px; + display: block; + color: #000000; + text-decoration: none; + font-weight: normal; + clear: both; + overflow: hidden; +} + +.event-box .location { + font-size: 18px; +} + +.event-box .location, .event-box .city, .event-box .date-text { + text-decoration: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.event-box .city { + color: #000000; + text-decoration: none; +} + +.event-box .date { + padding-top: 3px; + font-size: 100%; + font-weight: bold; +} + +@media (min-width: 980px) { + .event-box.has-picture .location, .event-box.has-picture .city, .event-box.has-picture .date { + margin-right: 50px; +} +} + +@media (max-width: 767px) { + .event-box.has-picture .location, .event-box.has-picture .city, .event-box.has-picture .date { + margin-right: 50px; +} +} + +.event-box-classification, .event-box-club-acronym { + vertical-align: 3px; +} + +#home_left { + padding-right: 4px; + width: 500px; + float: left; + text-align: left; +} +#home_right { + padding-left: 4px; + width: 227px; + float: right; + text-align: left; + border-left: 2px solid #325174; +} + +.flickr-photos .thumbnail div { + background-position: center; + background-size: cover; + width: 100%; + height: 180px; +} + +@media (max-width: 767px) { + .flickr-photos { + width: 100%; +} + +.flickr-photos > li { + width: 46%; +} + +.input-xxxlarge, { + display: block; + width: 100%; + min-height: 30px; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +} + +@media (max-width: 600px) { + .flickr-photos > li { + width: 100%; +} +} + + +/** Notices and Errors **/ +.alert { + margin: 5px; +} + +.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} + +.typeahead .active > a { + color: #fff; +} + +.full-width { + width: 100%; +} + +span.thin-control { + height: 18px; + line-height: 18px; + min-height: 18px; + padding: 0; + padding-left: 2px; + padding-right: 2px; + margin-bottom: 0; +} + +.input-group .time-spacer { + padding: 0; +} + +.input-group .time-segment { + width: 30px; + padding-left: 5px; + padding-right: 5px; +} + +.input-group .millisecond-time-segment { + padding-left: 5px; + padding-right: 5px; +} + +.wjr-help-tooltip:hover { + text-decoration: none; +} + +.wjr-help-tooltip > i { + vertical-align: middle; + font-size: 20px; + color: #888888; + padding-left: 4px; +} + +/* Hide the built in referral as we show it below the first feed items. + Also increase CSS specificity so this is actually used. */ +.juicer-feed .referral.referral { + display: none; +} \ No newline at end of file diff --git a/app/assets/stylesheets/clubsite/wysiwyg.css b/app/assets/stylesheets/clubsite/wysiwyg.css new file mode 100644 index 0000000..5253421 --- /dev/null +++ b/app/assets/stylesheets/clubsite/wysiwyg.css @@ -0,0 +1,6 @@ +@import url("bootstrap.min.css"); +@import url("whyjustrun.css"); + +body { + margin: 10px; +} diff --git a/app/assets/stylesheets/clubsite_embed.css b/app/assets/stylesheets/clubsite_embed.css new file mode 100644 index 0000000..0fbcb20 --- /dev/null +++ b/app/assets/stylesheets/clubsite_embed.css @@ -0,0 +1,6 @@ +/* + * Styles for the embeddable widget layout (embed.ctp). The layout also loads + * the main clubsite bundle (embed.ctp loaded layout_dependencies). + * + *= require clubsite/embed + */ diff --git a/app/assets/stylesheets/clubsite_other.css b/app/assets/stylesheets/clubsite_other.css new file mode 100644 index 0000000..73f0540 --- /dev/null +++ b/app/assets/stylesheets/clubsite_other.css @@ -0,0 +1,7 @@ +/* + * Additions for the "other" clubsite layout. The layout also loads the main + * clubsite bundle (mirrors other.ctp, which loaded layout_dependencies plus + * other.css). + * + *= require clubsite/other + */ diff --git a/app/assets/stylesheets/clubsite_printable.css b/app/assets/stylesheets/clubsite_printable.css new file mode 100644 index 0000000..3af63f4 --- /dev/null +++ b/app/assets/stylesheets/clubsite_printable.css @@ -0,0 +1,6 @@ +/* + * Styles for the printable layout. Mirrors printable.ctp, which loaded only + * printable.css (no bootstrap or whyjustrun styles). + * + *= require clubsite/printable + */ diff --git a/app/constraints/club_domain_constraint.rb b/app/constraints/club_domain_constraint.rb new file mode 100644 index 0000000..6aa59a9 --- /dev/null +++ b/app/constraints/club_domain_constraint.rb @@ -0,0 +1,10 @@ +# Routes inside this constraint serve club websites: any host other than the +# apex (Settings.host) is assumed to be a club domain. Club lookup happens in +# Clubsite::BaseController so routing stays free of database access. +class ClubDomainConstraint + def matches?(request) + # Settings.host may be configured with or without a port (e.g. localhost:3000) + apex = Settings.host + request.host != apex && request.host_with_port != apex + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 22cc58c..a7eb778 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,5 @@ class ApplicationController < ActionController::Base - include Pundit + include Pundit::Authorization protect_from_forgery with: :exception before_action :clear_redirect_club_if_necessary diff --git a/app/controllers/clubsite/base_controller.rb b/app/controllers/clubsite/base_controller.rb new file mode 100644 index 0000000..5235244 --- /dev/null +++ b/app/controllers/clubsite/base_controller.rb @@ -0,0 +1,62 @@ +module Clubsite + # Base for all controllers served on club domains. Resolves the club from the + # request host, runs the request in the club's timezone, and picks the club's + # layout. + class BaseController < ApplicationController + LAYOUTS = %w[default other].freeze + + before_action :set_current_club + around_action :use_club_time_zone + layout :club_layout + + helper_method :current_club, :club_resources + + rescue_from Pundit::NotAuthorizedError, with: :not_authorized + + private + + def set_current_club + # Ports include the port so development domains like demo.localhost:3000 work. + requested_host = request.host_with_port + @current_club = Club.find_by(domain: requested_host) + return if @current_club.present? + + redirected = Club.find_by(redirect_domain: requested_host) + if redirected + redirect_to "#{redirected.domain_protocol}://#{redirected.domain}#{request.fullpath}", + status: :moved_permanently, allow_other_host: true + else + render plain: "No club website exists for this domain. Contact support@whyjustrun.ca for help.", + status: :not_found + end + end + + def current_club + @current_club + end + + def club_resources + @club_resources ||= Resource.for_club(current_club) + end + + def use_club_time_zone(&block) + if current_club + Time.use_zone(current_club.timezone, &block) + else + yield + end + end + + def club_layout + return 'clubsite/embed' if request.format == :embed + + name = current_club&.layout + name = 'default' unless LAYOUTS.include?(name) + "clubsite/#{name}" + end + + def not_authorized + redirect_to '/', alert: 'You are not authorized to access that page. Please switch to a different account or ask your club webmaster for permissions' + end + end +end diff --git a/app/controllers/clubsite/errors_controller.rb b/app/controllers/clubsite/errors_controller.rb new file mode 100644 index 0000000..6c2971a --- /dev/null +++ b/app/controllers/clubsite/errors_controller.rb @@ -0,0 +1,8 @@ +module Clubsite + # Catch-all for club domains so requests never fall through to apex routes. + class ErrorsController < BaseController + def not_found + render plain: 'Not Found', status: :not_found + end + end +end diff --git a/app/controllers/clubsite/pages_controller.rb b/app/controllers/clubsite/pages_controller.rb new file mode 100644 index 0000000..8c899bb --- /dev/null +++ b/app/controllers/clubsite/pages_controller.rb @@ -0,0 +1,6 @@ +module Clubsite + class PagesController < BaseController + def home + end + end +end diff --git a/app/controllers/clubsite/robots_controller.rb b/app/controllers/clubsite/robots_controller.rb new file mode 100644 index 0000000..1f9791c --- /dev/null +++ b/app/controllers/clubsite/robots_controller.rb @@ -0,0 +1,10 @@ +module Clubsite + # Dynamic robots.txt: club sites are hidden from crawlers outside production + # and for clubs marked not visible. + class RobotsController < BaseController + def show + hidden = Rails.configuration.x.clubsite_robots_hidden || !current_club.visible + render plain: hidden ? "User-agent: *\nDisallow: /\n" : "User-agent: *\nDisallow:\n" + end + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 9b329f4..b1524d9 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -7,4 +7,14 @@ def about_whyjustrun def about_orienteering @top_level_clubs = Club.all_top_level.where(:visible => true) end + + def robots + render plain: <<~ROBOTS + # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file + # + # To ban all spiders from the entire site uncomment the next two lines: + # User-Agent: * + # Disallow: / + ROBOTS + end end diff --git a/app/helpers/clubsite/layout_helper.rb b/app/helpers/clubsite/layout_helper.rb new file mode 100644 index 0000000..3a1dd77 --- /dev/null +++ b/app/helpers/clubsite/layout_helper.rb @@ -0,0 +1,21 @@ +module Clubsite + module LayoutHelper + # Absolute URL to the apex (whyjustrun.ca) site, for cross-domain links + # from club pages. + def core_url(path = '/') + Settings.coreURL.chomp('/') + path + end + + def profile_url(user = current_user) + core_url("/users/#{user.id}") + end + + def admin_access? + user_signed_in? && current_user.has_privilege?(Settings.privileges.admin.page, current_club) + end + + def officials_access? + user_signed_in? && current_user.has_privilege?(Settings.privileges.official.edit, current_club) + end + end +end diff --git a/app/helpers/clubsite/menu_helper.rb b/app/helpers/clubsite/menu_helper.rb new file mode 100644 index 0000000..07a4b12 --- /dev/null +++ b/app/helpers/clubsite/menu_helper.rb @@ -0,0 +1,19 @@ +module Clubsite + module MenuHelper + def menu_item(name, url, css_class = '', home: false) + classes = [menu_active_class(url, home), css_class].reject(&:blank?).join(' ') + content_tag(:li, link_to(name, url), class: classes.presence) + end + + private + + def menu_active_class(path, home) + current = request.path + if home + ['', '/', '/pages/home'].include?(current) ? 'active' : nil + else + current == path ? 'active' : nil + end + end + end +end diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 0000000..f02ad25 --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,3 @@ +class Current < ActiveSupport::CurrentAttributes + attribute :club +end diff --git a/app/models/resource.rb b/app/models/resource.rb index 2c32ea3..bed8411 100644 --- a/app/models/resource.rb +++ b/app/models/resource.rb @@ -1,3 +1,44 @@ +require 'digest/md5' + +# Club branding assets (header image, logo, custom stylesheet) stored on the +# shared data volume as //. with jpg +# thumbnails alongside. class Resource < ApplicationRecord + THUMBNAILABLE_EXTENSIONS = %w[jpg jpeg gif png pdf].freeze + # Thumbnail widths in ImageMagick resize notation + THUMBNAIL_SIZES = %w[2600 1300 1000 500 100 50].freeze + belongs_to :club + + # Returns a map of resource keys to URLs, with additional _ + # entries for thumbnailable files. Used by club layouts. + def self.for_club(club) + map = {} + where(club_id: club.id).each do |resource| + map[resource.key] = resource.url + next unless resource.thumbnailable? + + THUMBNAIL_SIZES.each do |size| + map["#{resource.key}_#{size}"] = resource.url(size) + end + end + map + end + + def thumbnailable? + THUMBNAILABLE_EXTENSIONS.include?(extension) + end + + def url(thumbnail = nil) + cache_buster = Digest::MD5.hexdigest(updated_at.to_fs(:db)) + "#{Settings.dataURL}#{club_id}/#{relative_path(thumbnail)}?#{cache_buster}" + end + + def relative_path(thumbnail = nil) + if thumbnail + "#{key}_#{thumbnail}.jpg" + else + "#{key}.#{extension}" + end + end end diff --git a/app/views/clubsite/pages/home.html.erb b/app/views/clubsite/pages/home.html.erb new file mode 100644 index 0000000..3527ec9 --- /dev/null +++ b/app/views/clubsite/pages/home.html.erb @@ -0,0 +1,5 @@ +
+
+

<%= current_club.name %>

+
+
diff --git a/app/views/layouts/clubsite/_flash.html.erb b/app/views/layouts/clubsite/_flash.html.erb new file mode 100644 index 0000000..65c588e --- /dev/null +++ b/app/views/layouts/clubsite/_flash.html.erb @@ -0,0 +1 @@ +<%= bootstrap_flash %> diff --git a/app/views/layouts/clubsite/_head.html.erb b/app/views/layouts/clubsite/_head.html.erb new file mode 100644 index 0000000..9d5d21c --- /dev/null +++ b/app/views/layouts/clubsite/_head.html.erb @@ -0,0 +1,15 @@ + + + + + +<%= csrf_meta_tags %> +<%= stylesheet_link_tag 'clubsite', media: 'all' %> +<%= render 'layouts/clubsite/series_css' %> +<% if club_resources['style'].present? %> + <%# Custom club CSS uploaded through the admin interface; must load after the bundle so it wins %> + <%= stylesheet_link_tag club_resources['style'] %> +<% end %> +<%= yield :open_graph %> + + diff --git a/app/views/layouts/clubsite/_series_css.html.erb b/app/views/layouts/clubsite/_series_css.html.erb new file mode 100644 index 0000000..bb7ba0b --- /dev/null +++ b/app/views/layouts/clubsite/_series_css.html.erb @@ -0,0 +1,9 @@ +<% cache ['series_css', current_club], expires_in: 1.hour do %> + +<% end %> diff --git a/app/views/layouts/clubsite/default.html.erb b/app/views/layouts/clubsite/default.html.erb new file mode 100644 index 0000000..7546b79 --- /dev/null +++ b/app/views/layouts/clubsite/default.html.erb @@ -0,0 +1,77 @@ + + + + <%= content_for?(:title) ? yield(:title) : current_club.name %> + <%= render 'layouts/clubsite/head' %> + <%= yield :head %> + + + +
+
+ <%= render 'layouts/clubsite/flash' %> + <%= yield %> +
+
+
+
+ whyjustrun.ca + + <%= link_to 'API', 'https://github.com/WhyJustRun/Core/wiki/API' %> | + <%= link_to 'Get this website for your club', 'https://github.com/WhyJustRun/Core/wiki/Get-WhyJustRun-for-your-club!' %> + +
+
+ + © <%= Time.zone.now.year %> <%= current_club.name %> + + + By Thomas Nipen and Russell Porter | support@whyjustrun.ca + +
+
+ <%= javascript_include_tag 'clubsite' %> + <%= yield :javascript_bottom %> + + diff --git a/app/views/layouts/clubsite/embed.html.erb b/app/views/layouts/clubsite/embed.html.erb new file mode 100644 index 0000000..1b64651 --- /dev/null +++ b/app/views/layouts/clubsite/embed.html.erb @@ -0,0 +1,14 @@ + + + + <%= content_for?(:title) ? yield(:title) : current_club.name %> + <%= render 'layouts/clubsite/head' %> + <%= stylesheet_link_tag 'clubsite_embed', media: 'all' %> + <%= yield :head %> + + + <%= yield %> + <%= javascript_include_tag 'clubsite' %> + <%= yield :javascript_bottom %> + + diff --git a/app/views/layouts/clubsite/other.html.erb b/app/views/layouts/clubsite/other.html.erb new file mode 100644 index 0000000..9327649 --- /dev/null +++ b/app/views/layouts/clubsite/other.html.erb @@ -0,0 +1,55 @@ + + + + <%= content_for?(:title) ? yield(:title) : current_club.name %> + <%= render 'layouts/clubsite/head' %> + <%= stylesheet_link_tag 'clubsite_other', media: 'all' %> + <%= yield :head %> + + + +
+ <%= render 'layouts/clubsite/flash' %> + <%= yield %> +
+
+
+ © <%= Time.zone.now.year %> Orienteering Canada + By Thomas Nipen, Russell Porter + and Adrian Zissos +
+
+ <%= javascript_include_tag 'clubsite' %> + <%= yield :javascript_bottom %> + + diff --git a/app/views/layouts/clubsite/printable.html.erb b/app/views/layouts/clubsite/printable.html.erb new file mode 100644 index 0000000..3e78adb --- /dev/null +++ b/app/views/layouts/clubsite/printable.html.erb @@ -0,0 +1,15 @@ + + + + <%= content_for?(:title) ? yield(:title) : current_club.name %> + + + <%= stylesheet_link_tag 'clubsite_printable', media: 'all' %> + <%= yield :head %> + + + <%= yield %> + <%= javascript_include_tag 'clubsite' %> + <%= yield :javascript_bottom %> + + diff --git a/config/brakeman.ignore b/config/brakeman.ignore index c42ed2e..b05315e 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -11,6 +11,10 @@ { "fingerprint": "98b26f60d776fd41ee6f088c833725145be9aac2d7c5b33780241c273622db42", "note": "Rails 7.2 end-of-life is a known issue; the framework upgrade is tracked as follow-up work to the clubsite consolidation." + }, + { + "fingerprint": "dcd469c3af248cfe9ad0bd62ca6d04d009d336c4a057ed702eae420d78ad92f2", + "note": "Intentional 301 from a club's old domain to its current one; both values come from the clubs table, not from user input." } ], "updated": "2026-07-30 00:00:00 +0000", diff --git a/config/environments/development.rb b/config/environments/development.rb index 2e419c4..83908d1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -78,7 +78,11 @@ IPAddr.new("0.0.0.0/0"), # All IPv4 addresses. IPAddr.new("::/0"), # All IPv6 addresses. "localhost", # The localhost reserved domain. + ".localhost", # Development club domains, e.g. demo.localhost:3000 "host.docker.internal:3000", ENV["RAILS_DEVELOPMENT_HOSTS"] # Additional comma-separated hosts for development. ] + + # Club sites tell crawlers to stay away outside production + config.x.clubsite_robots_hidden = true end diff --git a/config/environments/production.rb b/config/environments/production.rb index 4dd71af..035c605 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -84,10 +84,13 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false - # Enable DNS rebinding protection and other `Host` header attacks. - config.hosts = ["whyjustrun.ca"] - # Skip DNS rebinding protection for the default health check endpoint. - config.host_authorization = { exclude: ->(request) { request.path == "/up" } } + # Club domains are arbitrary customer strings stored in the clubs table, so + # Rails cannot allowlist hosts. nginx is the Host allowlist: its vhosts are + # generated from the clubs table and a default_server rejects unknown hosts. + config.hosts.clear + + # Club sites are visible to crawlers in production (subject to clubs.visible) + config.x.clubsite_robots_hidden = false end Rails.application.config.middleware.use ExceptionNotification::Rack, diff --git a/config/environments/test.rb b/config/environments/test.rb index adbb4a6..1174140 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -11,6 +11,9 @@ # While tests run files are not watched, reloading is not necessary. config.enable_reloading = false + # Let tests exercise the clubs.visible logic in Clubsite::RobotsController + config.x.clubsite_robots_hidden = false + # Eager loading loads your entire application. When running a single test locally, # this is usually not necessary, and can slow down your test suite. However, it's # recommended that you enable it in continuous integration systems to ensure eager diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 19f51aa..042e2e8 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -12,3 +12,4 @@ # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) +Rails.application.config.assets.precompile += %w( clubsite.js clubsite.css clubsite_other.css clubsite_embed.css clubsite_printable.css ) diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index dc18996..d7d9a6a 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -2,3 +2,7 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf + +# Club pages rendered bare for ",error:'

The requested content cannot be loaded.
Please try again later.

',closeBtn:'',next:'',prev:''},openEffect:"fade",openSpeed:250,openEasing:"swing",openOpacity:!0, +openMethod:"zoomIn",closeEffect:"fade",closeSpeed:250,closeEasing:"swing",closeOpacity:!0,closeMethod:"zoomOut",nextEffect:"elastic",nextSpeed:250,nextEasing:"swing",nextMethod:"changeIn",prevEffect:"elastic",prevSpeed:250,prevEasing:"swing",prevMethod:"changeOut",helpers:{overlay:!0,title:!0},onCancel:f.noop,beforeLoad:f.noop,afterLoad:f.noop,beforeShow:f.noop,afterShow:f.noop,beforeChange:f.noop,beforeClose:f.noop,afterClose:f.noop},group:{},opts:{},previous:null,coming:null,current:null,isActive:!1, +isOpen:!1,isOpened:!1,wrap:null,skin:null,outer:null,inner:null,player:{timer:null,isActive:!1},ajaxLoad:null,imgPreload:null,transitions:{},helpers:{},open:function(a,d){if(a&&(f.isPlainObject(d)||(d={}),!1!==b.close(!0)))return f.isArray(a)||(a=t(a)?f(a).get():[a]),f.each(a,function(e,c){var k={},g,h,j,m,l;"object"===f.type(c)&&(c.nodeType&&(c=f(c)),t(c)?(k={href:c.data("fancybox-href")||c.attr("href"),title:c.data("fancybox-title")||c.attr("title"),isDom:!0,element:c},f.metadata&&f.extend(!0,k, +c.metadata())):k=c);g=d.href||k.href||(q(c)?c:null);h=d.title!==v?d.title:k.title||"";m=(j=d.content||k.content)?"html":d.type||k.type;!m&&k.isDom&&(m=c.data("fancybox-type"),m||(m=(m=c.prop("class").match(/fancybox\.(\w+)/))?m[1]:null));q(g)&&(m||(b.isImage(g)?m="image":b.isSWF(g)?m="swf":"#"===g.charAt(0)?m="inline":q(c)&&(m="html",j=c)),"ajax"===m&&(l=g.split(/\s+/,2),g=l.shift(),l=l.shift()));j||("inline"===m?g?j=f(q(g)?g.replace(/.*(?=#[^\s]+$)/,""):g):k.isDom&&(j=c):"html"===m?j=g:!m&&(!g&& +k.isDom)&&(m="inline",j=c));f.extend(k,{href:g,type:m,content:j,title:h,selector:l});a[e]=k}),b.opts=f.extend(!0,{},b.defaults,d),d.keys!==v&&(b.opts.keys=d.keys?f.extend({},b.defaults.keys,d.keys):!1),b.group=a,b._start(b.opts.index)},cancel:function(){var a=b.coming;a&&!1!==b.trigger("onCancel")&&(b.hideLoading(),b.ajaxLoad&&b.ajaxLoad.abort(),b.ajaxLoad=null,b.imgPreload&&(b.imgPreload.onload=b.imgPreload.onerror=null),a.wrap&&a.wrap.stop(!0,!0).trigger("onReset").remove(),b.coming=null,b.current|| +b._afterZoomOut(a))},close:function(a){b.cancel();!1!==b.trigger("beforeClose")&&(b.unbindEvents(),b.isActive&&(!b.isOpen||!0===a?(f(".fancybox-wrap").stop(!0).trigger("onReset").remove(),b._afterZoomOut()):(b.isOpen=b.isOpened=!1,b.isClosing=!0,f(".fancybox-item, .fancybox-nav").remove(),b.wrap.stop(!0,!0).removeClass("fancybox-opened"),b.transitions[b.current.closeMethod]())))},play:function(a){var d=function(){clearTimeout(b.player.timer)},e=function(){d();b.current&&b.player.isActive&&(b.player.timer= +setTimeout(b.next,b.current.playSpeed))},c=function(){d();p.unbind(".player");b.player.isActive=!1;b.trigger("onPlayEnd")};if(!0===a||!b.player.isActive&&!1!==a){if(b.current&&(b.current.loop||b.current.index=c.index?"next":"prev"],b.router=e||"jumpto",c.loop&&(0>a&&(a=c.group.length+a%c.group.length),a%=c.group.length),c.group[a]!==v&&(b.cancel(),b._start(a)))},reposition:function(a,d){var e=b.current,c=e?e.wrap:null,k;c&&(k=b._getPosition(d),a&&"scroll"===a.type?(delete k.position,c.stop(!0,!0).animate(k,200)):(c.css(k),e.pos=f.extend({},e.dim,k)))},update:function(a){var d= +a&&a.type,e=!d||"orientationchange"===d;e&&(clearTimeout(B),B=null);b.isOpen&&!B&&(B=setTimeout(function(){var c=b.current;c&&!b.isClosing&&(b.wrap.removeClass("fancybox-tmp"),(e||"load"===d||"resize"===d&&c.autoResize)&&b._setDimension(),"scroll"===d&&c.canShrink||b.reposition(a),b.trigger("onUpdate"),B=null)},e&&!s?0:300))},toggle:function(a){b.isOpen&&(b.current.fitToView="boolean"===f.type(a)?a:!b.current.fitToView,s&&(b.wrap.removeAttr("style").addClass("fancybox-tmp"),b.trigger("onUpdate")), +b.update())},hideLoading:function(){p.unbind(".loading");f("#fancybox-loading").remove()},showLoading:function(){var a,d;b.hideLoading();a=f('
').click(b.cancel).appendTo("body");p.bind("keydown.loading",function(a){if(27===(a.which||a.keyCode))a.preventDefault(),b.cancel()});b.defaults.fixed||(d=b.getViewport(),a.css({position:"absolute",top:0.5*d.h+d.y,left:0.5*d.w+d.x}))},getViewport:function(){var a=b.current&&b.current.locked||!1,d={x:n.scrollLeft(), +y:n.scrollTop()};a?(d.w=a[0].clientWidth,d.h=a[0].clientHeight):(d.w=s&&r.innerWidth?r.innerWidth:n.width(),d.h=s&&r.innerHeight?r.innerHeight:n.height());return d},unbindEvents:function(){b.wrap&&t(b.wrap)&&b.wrap.unbind(".fb");p.unbind(".fb");n.unbind(".fb")},bindEvents:function(){var a=b.current,d;a&&(n.bind("orientationchange.fb"+(s?"":" resize.fb")+(a.autoCenter&&!a.locked?" scroll.fb":""),b.update),(d=a.keys)&&p.bind("keydown.fb",function(e){var c=e.which||e.keyCode,k=e.target||e.srcElement; +if(27===c&&b.coming)return!1;!e.ctrlKey&&(!e.altKey&&!e.shiftKey&&!e.metaKey&&(!k||!k.type&&!f(k).is("[contenteditable]")))&&f.each(d,function(d,k){if(1h[0].clientWidth||h[0].clientHeight&&h[0].scrollHeight>h[0].clientHeight),h=f(h).parent();if(0!==c&&!j&&1g||0>k)b.next(0>g?"up":"right");d.preventDefault()}}))},trigger:function(a,d){var e,c=d||b.coming||b.current;if(c){f.isFunction(c[a])&&(e=c[a].apply(c,Array.prototype.slice.call(arguments,1)));if(!1===e)return!1;c.helpers&&f.each(c.helpers,function(d,e){if(e&&b.helpers[d]&&f.isFunction(b.helpers[d][a]))b.helpers[d][a](f.extend(!0, +{},b.helpers[d].defaults,e),c)});p.trigger(a)}},isImage:function(a){return q(a)&&a.match(/(^data:image\/.*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg)((\?|#).*)?$)/i)},isSWF:function(a){return q(a)&&a.match(/\.(swf)((\?|#).*)?$/i)},_start:function(a){var d={},e,c;a=l(a);e=b.group[a]||null;if(!e)return!1;d=f.extend(!0,{},b.opts,e);e=d.margin;c=d.padding;"number"===f.type(e)&&(d.margin=[e,e,e,e]);"number"===f.type(c)&&(d.padding=[c,c,c,c]);d.modal&&f.extend(!0,d,{closeBtn:!1,closeClick:!1,nextClick:!1,arrows:!1, +mouseWheel:!1,keys:null,helpers:{overlay:{closeClick:!1}}});d.autoSize&&(d.autoWidth=d.autoHeight=!0);"auto"===d.width&&(d.autoWidth=!0);"auto"===d.height&&(d.autoHeight=!0);d.group=b.group;d.index=a;b.coming=d;if(!1===b.trigger("beforeLoad"))b.coming=null;else{c=d.type;e=d.href;if(!c)return b.coming=null,b.current&&b.router&&"jumpto"!==b.router?(b.current.index=a,b[b.router](b.direction)):!1;b.isActive=!0;if("image"===c||"swf"===c)d.autoHeight=d.autoWidth=!1,d.scrolling="visible";"image"===c&&(d.aspectRatio= +!0);"iframe"===c&&s&&(d.scrolling="scroll");d.wrap=f(d.tpl.wrap).addClass("fancybox-"+(s?"mobile":"desktop")+" fancybox-type-"+c+" fancybox-tmp "+d.wrapCSS).appendTo(d.parent||"body");f.extend(d,{skin:f(".fancybox-skin",d.wrap),outer:f(".fancybox-outer",d.wrap),inner:f(".fancybox-inner",d.wrap)});f.each(["Top","Right","Bottom","Left"],function(a,b){d.skin.css("padding"+b,w(d.padding[a]))});b.trigger("onReady");if("inline"===c||"html"===c){if(!d.content||!d.content.length)return b._error("content")}else if(!e)return b._error("href"); +"image"===c?b._loadImage():"ajax"===c?b._loadAjax():"iframe"===c?b._loadIframe():b._afterLoad()}},_error:function(a){f.extend(b.coming,{type:"html",autoWidth:!0,autoHeight:!0,minWidth:0,minHeight:0,scrolling:"no",hasError:a,content:b.coming.tpl.error});b._afterLoad()},_loadImage:function(){var a=b.imgPreload=new Image;a.onload=function(){this.onload=this.onerror=null;b.coming.width=this.width/b.opts.pixelRatio;b.coming.height=this.height/b.opts.pixelRatio;b._afterLoad()};a.onerror=function(){this.onload= +this.onerror=null;b._error("image")};a.src=b.coming.href;!0!==a.complete&&b.showLoading()},_loadAjax:function(){var a=b.coming;b.showLoading();b.ajaxLoad=f.ajax(f.extend({},a.ajax,{url:a.href,error:function(a,e){b.coming&&"abort"!==e?b._error("ajax",a):b.hideLoading()},success:function(d,e){"success"===e&&(a.content=d,b._afterLoad())}}))},_loadIframe:function(){var a=b.coming,d=f(a.tpl.iframe.replace(/\{rnd\}/g,(new Date).getTime())).attr("scrolling",s?"auto":a.iframe.scrolling).attr("src",a.href); +f(a.wrap).bind("onReset",function(){try{f(this).find("iframe").hide().attr("src","//about:blank").end().empty()}catch(a){}});a.iframe.preload&&(b.showLoading(),d.one("load",function(){f(this).data("ready",1);s||f(this).bind("load.fb",b.update);f(this).parents(".fancybox-wrap").width("100%").removeClass("fancybox-tmp").show();b._afterLoad()}));a.content=d.appendTo(a.inner);a.iframe.preload||b._afterLoad()},_preloadImages:function(){var a=b.group,d=b.current,e=a.length,c=d.preload?Math.min(d.preload, +e-1):0,f,g;for(g=1;g<=c;g+=1)f=a[(d.index+g)%e],"image"===f.type&&f.href&&((new Image).src=f.href)},_afterLoad:function(){var a=b.coming,d=b.current,e,c,k,g,h;b.hideLoading();if(a&&!1!==b.isActive)if(!1===b.trigger("afterLoad",a,d))a.wrap.stop(!0).trigger("onReset").remove(),b.coming=null;else{d&&(b.trigger("beforeChange",d),d.wrap.stop(!0).removeClass("fancybox-opened").find(".fancybox-item, .fancybox-nav").remove());b.unbindEvents();e=a.content;c=a.type;k=a.scrolling;f.extend(b,{wrap:a.wrap,skin:a.skin, +outer:a.outer,inner:a.inner,current:a,previous:d});g=a.href;switch(c){case "inline":case "ajax":case "html":a.selector?e=f("
").html(e).find(a.selector):t(e)&&(e.data("fancybox-placeholder")||e.data("fancybox-placeholder",f('
').insertAfter(e).hide()),e=e.show().detach(),a.wrap.bind("onReset",function(){f(this).find(e).length&&e.hide().replaceAll(e.data("fancybox-placeholder")).data("fancybox-placeholder",!1)}));break;case "image":e=a.tpl.image.replace("{href}", +g);break;case "swf":e='',h="",f.each(a.swf,function(a,b){e+='';h+=" "+a+'="'+b+'"'}),e+='"}(!t(e)||!e.parent().is(a.inner))&&a.inner.append(e);b.trigger("beforeShow");a.inner.css("overflow","yes"===k?"scroll": +"no"===k?"hidden":k);b._setDimension();b.reposition();b.isOpen=!1;b.coming=null;b.bindEvents();if(b.isOpened){if(d.prevMethod)b.transitions[d.prevMethod]()}else f(".fancybox-wrap").not(a.wrap).stop(!0).trigger("onReset").remove();b.transitions[b.isOpened?a.nextMethod:a.openMethod]();b._preloadImages()}},_setDimension:function(){var a=b.getViewport(),d=0,e=!1,c=!1,e=b.wrap,k=b.skin,g=b.inner,h=b.current,c=h.width,j=h.height,m=h.minWidth,u=h.minHeight,n=h.maxWidth,p=h.maxHeight,s=h.scrolling,q=h.scrollOutside? +h.scrollbarWidth:0,x=h.margin,y=l(x[1]+x[3]),r=l(x[0]+x[2]),v,z,t,C,A,F,B,D,H;e.add(k).add(g).width("auto").height("auto").removeClass("fancybox-tmp");x=l(k.outerWidth(!0)-k.width());v=l(k.outerHeight(!0)-k.height());z=y+x;t=r+v;C=E(c)?(a.w-z)*l(c)/100:c;A=E(j)?(a.h-t)*l(j)/100:j;if("iframe"===h.type){if(H=h.content,h.autoHeight&&1===H.data("ready"))try{H[0].contentWindow.document.location&&(g.width(C).height(9999),F=H.contents().find("body"),q&&F.css("overflow-x","hidden"),A=F.outerHeight(!0))}catch(G){}}else if(h.autoWidth|| +h.autoHeight)g.addClass("fancybox-tmp"),h.autoWidth||g.width(C),h.autoHeight||g.height(A),h.autoWidth&&(C=g.width()),h.autoHeight&&(A=g.height()),g.removeClass("fancybox-tmp");c=l(C);j=l(A);D=C/A;m=l(E(m)?l(m,"w")-z:m);n=l(E(n)?l(n,"w")-z:n);u=l(E(u)?l(u,"h")-t:u);p=l(E(p)?l(p,"h")-t:p);F=n;B=p;h.fitToView&&(n=Math.min(a.w-z,n),p=Math.min(a.h-t,p));z=a.w-y;r=a.h-r;h.aspectRatio?(c>n&&(c=n,j=l(c/D)),j>p&&(j=p,c=l(j*D)),cz||y>r)&&(c>m&&j>u)&&!(19n&&(c=n,j=l(c/D)),g.width(c).height(j),e.width(c+x),a=e.width(),y=e.height();else c=Math.max(m,Math.min(c,c-(a-z))),j=Math.max(u,Math.min(j,j-(y-r)));q&&("auto"===s&&jz||y>r)&&c>m&&j>u;c=h.aspectRatio?cu&&j
').appendTo(b.coming?b.coming.parent:a.parent);this.fixed=!1;a.fixed&&b.defaults.fixed&&(this.overlay.addClass("fancybox-overlay-fixed"),this.fixed=!0)},open:function(a){var d=this;a=f.extend({},this.defaults,a);this.overlay?this.overlay.unbind(".overlay").width("auto").height("auto"):this.create(a);this.fixed||(n.bind("resize.overlay",f.proxy(this.update,this)),this.update());a.closeClick&&this.overlay.bind("click.overlay",function(a){if(f(a.target).hasClass("fancybox-overlay"))return b.isActive? +b.close():d.close(),!1});this.overlay.css(a.css).show()},close:function(){var a,b;n.unbind("resize.overlay");this.el.hasClass("fancybox-lock")&&(f(".fancybox-margin").removeClass("fancybox-margin"),a=n.scrollTop(),b=n.scrollLeft(),this.el.removeClass("fancybox-lock"),n.scrollTop(a).scrollLeft(b));f(".fancybox-overlay").remove().hide();f.extend(this,{overlay:null,fixed:!1})},update:function(){var a="100%",b;this.overlay.width(a).height("100%");I?(b=Math.max(G.documentElement.offsetWidth,G.body.offsetWidth), +p.width()>b&&(a=p.width())):p.width()>n.width()&&(a=p.width());this.overlay.width(a).height(p.height())},onReady:function(a,b){var e=this.overlay;f(".fancybox-overlay").stop(!0,!0);e||this.create(a);a.locked&&(this.fixed&&b.fixed)&&(e||(this.margin=p.height()>n.height()?f("html").css("margin-right").replace("px",""):!1),b.locked=this.overlay.append(b.wrap),b.fixed=!1);!0===a.showEarly&&this.beforeShow.apply(this,arguments)},beforeShow:function(a,b){var e,c;b.locked&&(!1!==this.margin&&(f("*").filter(function(){return"fixed"=== +f(this).css("position")&&!f(this).hasClass("fancybox-overlay")&&!f(this).hasClass("fancybox-wrap")}).addClass("fancybox-margin"),this.el.addClass("fancybox-margin")),e=n.scrollTop(),c=n.scrollLeft(),this.el.addClass("fancybox-lock"),n.scrollTop(e).scrollLeft(c));this.open(a)},onUpdate:function(){this.fixed||this.update()},afterClose:function(a){this.overlay&&!b.coming&&this.overlay.fadeOut(a.speedOut,f.proxy(this.close,this))}};b.helpers.title={defaults:{type:"float",position:"bottom"},beforeShow:function(a){var d= +b.current,e=d.title,c=a.type;f.isFunction(e)&&(e=e.call(d.element,d));if(q(e)&&""!==f.trim(e)){d=f('
'+e+"
");switch(c){case "inside":c=b.skin;break;case "outside":c=b.wrap;break;case "over":c=b.inner;break;default:c=b.skin,d.appendTo("body"),I&&d.width(d.width()),d.wrapInner(''),b.current.margin[2]+=Math.abs(l(d.css("margin-bottom")))}d["top"===a.position?"prependTo":"appendTo"](c)}}};f.fn.fancybox=function(a){var d, +e=f(this),c=this.selector||"",k=function(g){var h=f(this).blur(),j=d,k,l;!g.ctrlKey&&(!g.altKey&&!g.shiftKey&&!g.metaKey)&&!h.is(".fancybox-wrap")&&(k=a.groupAttr||"data-fancybox-group",l=h.attr(k),l||(k="rel",l=h.get(0)[k]),l&&(""!==l&&"nofollow"!==l)&&(h=c.length?f(c):e,h=h.filter("["+k+'="'+l+'"]'),j=h.index(this)),a.index=j,!1!==b.open(h,a)&&g.preventDefault())};a=a||{};d=a.index||0;!c||!1===a.live?e.unbind("click.fb-start").bind("click.fb-start",k):p.undelegate(c,"click.fb-start").delegate(c+ +":not('.fancybox-item, .fancybox-nav')","click.fb-start",k);this.filter("[data-fancybox-start=1]").trigger("click");return this};p.ready(function(){var a,d;f.scrollbarWidth===v&&(f.scrollbarWidth=function(){var a=f('
').appendTo("body"),b=a.children(),b=b.innerWidth()-b.height(99).innerWidth();a.remove();return b});if(f.support.fixedPosition===v){a=f.support;d=f('
').appendTo("body");var e=20=== +d[0].offsetTop||15===d[0].offsetTop;d.remove();a.fixedPosition=e}f.extend(b.defaults,{scrollbarWidth:f.scrollbarWidth(),fixed:f.support.fixedPosition,parent:f("body")});a=f(r).width();J.addClass("fancybox-lock-test");d=f(r).width();J.removeClass("fancybox-lock-test");f("").appendTo("head")})})(window,document,jQuery); diff --git a/vendor/assets/javascripts/clubsite/jquery.jeditable.js b/vendor/assets/javascripts/clubsite/jquery.jeditable.js new file mode 100644 index 0000000..258cde4 --- /dev/null +++ b/vendor/assets/javascripts/clubsite/jquery.jeditable.js @@ -0,0 +1,38 @@ +// NOTE: Source has been modified from original +(function($){$.fn.editable=function(target,options){if('disable'==target){$(this).data('disabled.editable',true);return;} +if('enable'==target){$(this).data('disabled.editable',false);return;} +if('destroy'==target){$(this).unbind($(this).data('event.editable')).removeData('disabled.editable').removeData('event.editable');return;} +var settings=$.extend({},$.fn.editable.defaults,{target:target},options);var plugin=$.editable.types[settings.type].plugin||function(){};var submit=$.editable.types[settings.type].submit||function(){};var buttons=$.editable.types[settings.type].buttons||$.editable.types['defaults'].buttons;var content=$.editable.types[settings.type].content||$.editable.types['defaults'].content;var element=$.editable.types[settings.type].element||$.editable.types['defaults'].element;var reset=$.editable.types[settings.type].reset||$.editable.types['defaults'].reset;var callback=settings.callback||function(){};var onedit=settings.onedit||function(){};var onsubmit=settings.onsubmit||function(){};var onreset=settings.onreset||function(){};var onerror=settings.onerror||reset;if(settings.tooltip){$(this).attr('title',settings.tooltip);} +settings.autowidth='auto'==settings.width;settings.autoheight='auto'==settings.height;return this.each(function(){var self=this;var savedwidth=$(self).width();var savedheight=$(self).height();$(this).data('event.editable',settings.event);if(!$.trim($(this).html())){$(this).html(settings.placeholder);} +$(this).bind(settings.event,function(e){if(true===$(this).data('disabled.editable')){return;} +if(self.editing){return;} +if(false===onedit.apply(this,[settings,self])){return;} +e.preventDefault();e.stopPropagation();if(settings.tooltip){$(self).removeAttr('title');} +if(0==$(self).width()){settings.width=savedwidth;settings.height=savedheight;}else{if(settings.width!='none'){settings.width=settings.autowidth?$(self).width():settings.width;} +if(settings.height!='none'){settings.height=settings.autoheight?$(self).height():settings.height;}} +if($(this).html().toLowerCase().replace(/(;|")/g,'')==settings.placeholder.toLowerCase().replace(/(;|")/g,'')){$(this).html('');} +self.editing=true;self.revert=$(self).html();$(self).html('');var form=$('
');if(settings.cssclass){if('inherit'==settings.cssclass){form.attr('class',$(self).attr('class'));}else{form.attr('class',settings.cssclass);}} +if(settings.style){if('inherit'==settings.style){form.attr('style',$(self).attr('style'));form.css('display',$(self).css('display'));}else{form.attr('style',settings.style);}} +var input=element.apply(form,[settings,self]);var input_content;if(settings.loadurl){var t=setTimeout(function(){input.disabled=true;content.apply(form,[settings.loadtext,settings,self]);},100);var loaddata={};loaddata[settings.id]=self.id;if($.isFunction(settings.loaddata)){$.extend(loaddata,settings.loaddata.apply(self,[self.revert,settings]));}else{$.extend(loaddata,settings.loaddata);} +$.ajax({type:settings.loadtype,url:settings.loadurl,data:loaddata,async:false,success:function(result){window.clearTimeout(t);input_content=result;input.disabled=false;}});}else if(settings.data){input_content=settings.data;if($.isFunction(settings.data)){input_content=settings.data.apply(self,[self.revert,settings]);}}else{input_content=self.revert;} +content.apply(form,[input_content,settings,self]);input.attr('name',settings.name);buttons.apply(form,[settings,self]);$(self).append(form);plugin.apply(form,[settings,self]);$(':input:visible:enabled:first',form).focus();if(settings.select){input.select();} +input.keydown(function(e){if(e.keyCode==27){e.preventDefault();reset.apply(form,[settings,self]);}});var t;if('cancel'==settings.onblur){input.blur(function(e){t=setTimeout(function(){reset.apply(form,[settings,self]);},500);});}else if('submit'==settings.onblur){input.blur(function(e){t=setTimeout(function(){form.submit();},200);});}else if($.isFunction(settings.onblur)){input.blur(function(e){settings.onblur.apply(self,[input.val(),settings]);});}else{input.blur(function(e){});} +form.submit(function(e){if(t){clearTimeout(t);} +e.preventDefault();if(false!==onsubmit.apply(form,[settings,self])){if(false!==submit.apply(form,[settings,self])){if($.isFunction(settings.target)){var str=settings.target.apply(self,[input.val(),settings]);$(self).html(str);self.editing=false;callback.apply(self,[self.innerHTML,settings]);if(!$.trim($(self).html())){$(self).html(settings.placeholder);}}else{var submitdata={};submitdata[settings.name]=input.val();submitdata[settings.id]=self.id;if($.isFunction(settings.submitdata)){$.extend(submitdata,settings.submitdata.apply(self,[self.revert,settings]));}else{$.extend(submitdata,settings.submitdata);} +if('PUT'==settings.method){submitdata['_method']='put';} +$(self).html(settings.indicator);var ajaxoptions={type:'POST',data:submitdata,dataType:'html',url:settings.target,success:function(result,status){if(ajaxoptions.dataType=='html'){$(self).html(result);} +self.editing=false;callback.apply(self,[result,settings]);if(!$.trim($(self).html())){$(self).html(settings.placeholder);}},error:function(xhr,status,error){onerror.apply(form,[settings,self,xhr]);}};$.extend(ajaxoptions,settings.ajaxoptions);$.ajax(ajaxoptions);}}} +$(self).attr('title',settings.tooltip);return false;});});this.reset=function(form){if(this.editing){if(false!==onreset.apply(form,[settings,self])){$(self).html(self.revert);self.editing=false;if(!$.trim($(self).html())){$(self).html(settings.placeholder);} +if(settings.tooltip){$(self).attr('title',settings.tooltip);}}}};});};$.editable={types:{defaults:{element:function(settings,original){var input=$('');$(this).append(input);return(input);},content:function(string,settings,original){$(':input:first',this).val(string);},reset:function(settings,original){original.reset(this);},buttons:function(settings,original){var form=this;if(settings.submit){if(settings.submit.match(/>$/)){var submit=$(settings.submit).click(function(){if(submit.attr("type")!="submit"){form.submit();}});}else{var submit=$('
+ + diff --git a/app/views/clubsite/events/_list.html.erb b/app/views/clubsite/events/_list.html.erb new file mode 100644 index 0000000..17cce24 --- /dev/null +++ b/app/views/clubsite/events/_list.html.erb @@ -0,0 +1,29 @@ +<%# Knockout event list fed by the IOF XML event feed. Self-contained so the + home page can render it too. %> +
+
+ +
"> +

Showing events after

+
+

Showing events before

+
+ + <%= render 'clubsite/events/knockout_box', template_name: 'event-box-template' %> +
+
diff --git a/app/views/clubsite/events/_list_header.html.erb b/app/views/clubsite/events/_list_header.html.erb new file mode 100644 index 0000000..b73d17d --- /dev/null +++ b/app/views/clubsite/events/_list_header.html.erb @@ -0,0 +1,24 @@ +<%# locals: type ('calendar' or 'list') %> + diff --git a/app/views/clubsite/events/_live_result_list.html.erb b/app/views/clubsite/events/_live_result_list.html.erb new file mode 100644 index 0000000..218b0ad --- /dev/null +++ b/app/views/clubsite/events/_live_result_list.html.erb @@ -0,0 +1,12 @@ +<%# locals: event %> +

+ Live results - these are not finalized! +
+ Results will automatically update, you don't need to refresh your browser. +

+ +
+
" data-result-list-mode="live"> + <%= render 'knockout_result_list', mode: 'live' %> +
+
diff --git a/app/views/clubsite/events/_live_results_visibility_button.html.erb b/app/views/clubsite/events/_live_results_visibility_button.html.erb new file mode 100644 index 0000000..a4f801e --- /dev/null +++ b/app/views/clubsite/events/_live_results_visibility_button.html.erb @@ -0,0 +1,6 @@ +<%# locals: event, result_list. Editors only; the toggle endpoint is ported + separately. %> +<%= button_to result_list.visible ? 'Hide Live Results' : 'Show Live Results', + "/events/toggle_live_results_visibility/#{event.id}/#{result_list.visible ? 'false' : 'true'}", + class: 'btn btn-primary' %> +

diff --git a/app/views/clubsite/events/_open_graph_tags.html.erb b/app/views/clubsite/events/_open_graph_tags.html.erb new file mode 100644 index 0000000..9c4b512 --- /dev/null +++ b/app/views/clubsite/events/_open_graph_tags.html.erb @@ -0,0 +1,18 @@ +<%# locals: event %> +<% + dynamic_text = "Orienteering event taking place: #{formatted_event_date(event)}." + finish_time = event.read_attribute(:finish_date) + + og_tag('og:site_name', 'WhyJustRun') + og_tag('og:type', 'event') + og_tag('og:url', current_club.clubsite_url("/events/view/#{event.id}")) + og_tag('og:description', "#{dynamic_text} Orienteering is an exciting sport for all ages and fitness levels that involves reading a detailed map and using a compass to find checkpoints.") + og_tag('og:title', event.name) + og_tag('og:image', current_club.clubsite_url('/img/orienteering_symbol.png')) + og_tag('event:start_time', event.date.iso8601) + og_tag('event:end_time', finish_time.iso8601) if finish_time + if event.lat.present? + og_tag('event:location:latitude', event.lat) + og_tag('event:location:longitude', event.lng) + end +%> diff --git a/app/views/clubsite/events/_organizers_list.html.erb b/app/views/clubsite/events/_organizers_list.html.erb new file mode 100644 index 0000000..fa0234b --- /dev/null +++ b/app/views/clubsite/events/_organizers_list.html.erb @@ -0,0 +1,2 @@ +<%# locals: organizers %> +<% organizers.each_with_index do |organizer, index| %><%= ', ' if index.positive? %><%= link_to organizer.user.name, profile_url(organizer.user) %><%= " (#{organizer.role.name})" if organizer.role.present? %><% end %> diff --git a/app/views/clubsite/events/_redirect_view.html.erb b/app/views/clubsite/events/_redirect_view.html.erb new file mode 100644 index 0000000..2ea5e0c --- /dev/null +++ b/app/views/clubsite/events/_redirect_view.html.erb @@ -0,0 +1,13 @@ +<%# locals: event, can_edit. Only editors see this page; everybody else was + redirected to the event's custom URL by the controller. %> +<%= render 'header', event: event, can_edit: can_edit %> + +
+ IMPORTANT +

You are seeing this page because you can edit this event. Normal users will be redirected automatically to the external event page. To start showing this WhyJustRun event page to normal users, remove the Redirect URL on the edit event page.

+
+
+

+ Continue to Event + Edit Event +

diff --git a/app/views/clubsite/events/_registration_section.html.erb b/app/views/clubsite/events/_registration_section.html.erb new file mode 100644 index 0000000..2b5e94d --- /dev/null +++ b/app/views/clubsite/events/_registration_section.html.erb @@ -0,0 +1,29 @@ +<%# locals: event, can_edit %> +<% result_list = event.result_list %> +<% if result_list.present? && result_list.status == ResultList::LIVE_STATUS && can_edit %> +

Live Results

+ <%= render 'live_results_visibility_button', event: event, result_list: result_list %> +<% end %> + +<% if @completed %> + <%= render 'course_maps', event: event %> +<% elsif event.registration_url.present? %> +
+

Registration

+
+ +<% elsif event.courses.any? %> +
+

Course Registration

+ <% if event.registration_deadline.present? %> +

+ <%= @registration_open ? "Deadline #{short_date_with_time(event.registration_deadline)}" : 'Registration is Closed' %> +

+ <% end %> +
+ <%= render 'course_registration', event: event %> +<% end %> diff --git a/app/views/clubsite/events/_results_entries.html.erb b/app/views/clubsite/events/_results_entries.html.erb new file mode 100644 index 0000000..72294c7 --- /dev/null +++ b/app/views/clubsite/events/_results_entries.html.erb @@ -0,0 +1,44 @@ +<%# locals: course, results. The list of registrations for a course. %> +<% if results.present? %> + + + + + + + + <% user_id = current_user&.id %> + <% results.each do |result| %> + <% own_entry = user_id.present? && (result.registrant_id == user_id || result.user_id == user_id) %> + <% modal_id = own_entry ? "change-comment-modal-#{result.id}" : nil %> + + + + <% end %> + +
<%= results.length == 1 ? 'Entries' : "Entries (#{results.length})" %>
+ <%= link_to result.user.name, profile_url(result.user) %> + +
+ <% if own_entry %> +
+ +
+
+ <%= button_to "/courses/unregister/#{course.id}/#{result.user_id}", class: 'btn btn-xs btn-danger' do %> + Unregister + <% end %> +
+ <% end %> + <% if result.registrant_comment.present? %> +
+ +
+ <% end %> +
+
+ <% if modal_id %> + <%= render 'change_comment_modal', result: result, modal_id: modal_id %> + <% end %> +
+<% end %> diff --git a/app/views/clubsite/events/_results_links.html.erb b/app/views/clubsite/events/_results_links.html.erb new file mode 100644 index 0000000..06f5f39 --- /dev/null +++ b/app/views/clubsite/events/_results_links.html.erb @@ -0,0 +1,9 @@ +<%# locals: event %> +<% { results_url: 'Results at ', routegadget_url: 'RouteGadget at ' }.each do |url_key, title_prefix| %> + <% url = event.public_send(url_key) %> + <% next if url.blank? %> + +

+<% end %> diff --git a/app/views/clubsite/events/_results_list.html.erb b/app/views/clubsite/events/_results_list.html.erb new file mode 100644 index 0000000..c8d15dc --- /dev/null +++ b/app/views/clubsite/events/_results_list.html.erb @@ -0,0 +1,6 @@ +<%# locals: event_id %> +
+
"> + <%= render 'knockout_result_list', mode: 'normal' %> +
+
diff --git a/app/views/clubsite/events/_results_section.html.erb b/app/views/clubsite/events/_results_section.html.erb new file mode 100644 index 0000000..4fe96e6 --- /dev/null +++ b/app/views/clubsite/events/_results_section.html.erb @@ -0,0 +1,22 @@ +<%# locals: event, can_edit %> +
+
+

Results

+
+ + <% result_list = event.result_list %> + <% if event.results_posted %> + <%= render 'results_list', event_id: event.id %> + <% elsif result_list.present? && result_list.status == ResultList::LIVE_STATUS %> + <% if can_edit %> + <%= render 'live_results_visibility_button', event: event, result_list: result_list %> + <% end %> + + <% if result_list.visible %> + <%= render 'live_result_list', event: event %> + <% end %> + <% end %> + + <%= render 'results_links', event: event %> + <%= render 'course_maps', event: event %> +
diff --git a/app/views/clubsite/events/_summary_list.html.erb b/app/views/clubsite/events/_summary_list.html.erb new file mode 100644 index 0000000..4bd8140 --- /dev/null +++ b/app/views/clubsite/events/_summary_list.html.erb @@ -0,0 +1,12 @@ +<% + sections = { + 'Ongoing' => current_club.events.list_includes.ongoing.to_a, + 'Upcoming' => current_club.events.list_includes.upcoming.limit(4).to_a, + 'Past' => current_club.events.list_includes.past.limit(2).to_a + } +%> +<% sections.each do |title, events| %> + <% next if events.empty? %> +

<%= title %>

+ <%= render 'clubsite/events/box_list', events: events %> +<% end %> diff --git a/app/views/clubsite/events/_summary_list_by_series.html.erb b/app/views/clubsite/events/_summary_list_by_series.html.erb new file mode 100644 index 0000000..d767b74 --- /dev/null +++ b/app/views/clubsite/events/_summary_list_by_series.html.erb @@ -0,0 +1,34 @@ +<% ongoing = current_club.events.list_includes.ongoing.to_a %> +<% if ongoing.any? %> +

Ongoing

+ <%= render 'clubsite/events/box_list', events: ongoing %> +<% end %> + +<% current_series = current_club.series.where(is_current: true) %> +<% + upcoming = {} + current_series.each do |series| + upcoming[series.name] = current_club.events.list_includes.upcoming.for_series(series.id).limit(2).to_a + end + upcoming['Other'] = current_club.events.list_includes.upcoming.for_series(0).limit(2).to_a +%> +

Upcoming

+<% upcoming.each do |title, events| %> + <% next if events.empty? %> +

<%= title %>

+ <%= render 'clubsite/events/box_list', events: events %> +<% end %> + +<% + past = {} + current_series.each do |series| + past[series.name] = current_club.events.list_includes.past.for_series(series.id).limit(1).to_a + end + past['Other'] = current_club.events.list_includes.past.for_series(0).limit(1).to_a +%> +

Past

+<% past.each do |title, events| %> + <% next if events.empty? %> +

<%= title %>

+ <%= render 'clubsite/events/box_list', events: events %> +<% end %> diff --git a/app/views/clubsite/events/index.html.erb b/app/views/clubsite/events/index.html.erb new file mode 100644 index 0000000..5ca2661 --- /dev/null +++ b/app/views/clubsite/events/index.html.erb @@ -0,0 +1,10 @@ +<% content_for :title, 'Calendar' %> +<%= render 'list_header', type: 'calendar' %> +<%= render 'calendar', year: @year, month: @month, day: @day %> +<% if @series.any? %> +
    + <% @series.each do |series| %> +
  • <%= series.name %>
  • + <% end %> +
+<% end %> diff --git a/app/views/clubsite/events/listing.html.erb b/app/views/clubsite/events/listing.html.erb new file mode 100644 index 0000000..31c6e28 --- /dev/null +++ b/app/views/clubsite/events/listing.html.erb @@ -0,0 +1,2 @@ +<%= render 'list_header', type: 'list' %> +<%= render 'list' %> diff --git a/app/views/clubsite/events/map.html.erb b/app/views/clubsite/events/map.html.erb new file mode 100644 index 0000000..d833541 --- /dev/null +++ b/app/views/clubsite/events/map.html.erb @@ -0,0 +1,13 @@ + + +
+
diff --git a/app/views/clubsite/events/show.html.erb b/app/views/clubsite/events/show.html.erb new file mode 100644 index 0000000..46821ad --- /dev/null +++ b/app/views/clubsite/events/show.html.erb @@ -0,0 +1,30 @@ +<% content_for :title, @event.name %> +<% if @event.custom_url.blank? %> + <%= render 'open_graph_tags', event: @event %> + <%= render 'header', event: @event, can_edit: @can_edit %> + +
+ <% if !@has_results %> + <%# Pre-event template %> +
+ <%= render 'info', event: @event %> +
+ +
+ <%= render 'registration_section', event: @event, can_edit: @can_edit %> +
+ <% else %> + <%# Post-event template %> +
+ <%= render 'results_section', event: @event, can_edit: @can_edit %> +
+
+ <%= render 'info', event: @event %> +
+ <% end %> +
+ + <%= render 'clubsite/shared/flickr_photos', event_id: @event.id %> +<% else %> + <%= render 'redirect_view', event: @event, can_edit: @can_edit %> +<% end %> diff --git a/app/views/clubsite/maps/_events_on_map.html.erb b/app/views/clubsite/maps/_events_on_map.html.erb new file mode 100644 index 0000000..8ffd119 --- /dev/null +++ b/app/views/clubsite/maps/_events_on_map.html.erb @@ -0,0 +1,19 @@ +<% if events.any? %> +

Events on this map

+ + + + + + + + + <% events.each do |event| %> + + + + + <% end %> + +
SeriesDate
<%= event.series&.acronym.present? ? "#{event.name} (#{event.series.acronym})" : event.name %><%= link_to event.local_date.strftime('%b %-d, %Y'), clubsite_event_path(id: event.id) %>
+<% end %> diff --git a/app/views/clubsite/maps/_info.html.erb b/app/views/clubsite/maps/_info.html.erb new file mode 100644 index 0000000..1f97bd4 --- /dev/null +++ b/app/views/clubsite/maps/_info.html.erb @@ -0,0 +1,9 @@ +<% if map.lat.present? %> +

Location

+ <% location = geocode_look_up(map.lat, map.lng) %> + <% location_name = [location['neighbourhood'], location['city']].reject(&:blank?).join(', ') %> + <% if location_name.present? %> +

<%= location_name %>

+ <% end %> + +<% end %> diff --git a/app/views/clubsite/maps/index.html.erb b/app/views/clubsite/maps/index.html.erb new file mode 100644 index 0000000..5a86c68 --- /dev/null +++ b/app/views/clubsite/maps/index.html.erb @@ -0,0 +1,28 @@ + +
+
+ <%= render_content_blocks 'general_maps_information', nil, '
' %> +
+
+
+
+
+
diff --git a/app/views/clubsite/maps/report.html.erb b/app/views/clubsite/maps/report.html.erb new file mode 100644 index 0000000..e69d4e6 --- /dev/null +++ b/app/views/clubsite/maps/report.html.erb @@ -0,0 +1,36 @@ + + +
+ + + + + + + + + + + + + + + + <% @maps.each_with_index do |map, index| %> + + + + + + + + + + + + <% end %> + +
NameMap standardCreatedModifiedScaleLatitudeLongitudeThumbnail
<%= index + 1 %><%= link_to map.name, clubsite_map_path(id: map.id) %><%= map.map_standard&.name %><%= map.created&.strftime('%Y-%m-%d') %><%= map.modified&.strftime('%Y-%m-%d') %><%= "1:#{number_with_delimiter(map.scale)}" if map.scale.present? && map.scale.nonzero? %><%= format('%.3f', map.lat.to_f) %><%= format('%.3f', map.lng.to_f) %><%= media_image('Map', map.id, '60x60') if media_exists?('Map', map.id, '60x60') %>
+
diff --git a/app/views/clubsite/maps/show.html.erb b/app/views/clubsite/maps/show.html.erb new file mode 100644 index 0000000..f4c341c --- /dev/null +++ b/app/views/clubsite/maps/show.html.erb @@ -0,0 +1,51 @@ +<% content_for :title, @map.name %> + +
+
+

Statistics

+ + <% if @map.scale.present? && @map.scale.nonzero? %> + + <% end %> + + +
Scale1:<%= number_with_delimiter(@map.scale) %>
Map standard<%= @map.map_standard&.name %>
Events on map<%= @events.size %>
+
+ <% if @map.notes.present? %> +

Notes

+ <%= sanitize @map.notes %> +
+ <% end %> +

Map image

+ <% if media_exists?('Map', @map.id) %> + <%= media_linked_image('Map', @map.id, '400x600', {}, class: 'fitting-image') %> + <% else %> + No map image available. + <% end %> +
+ <%= render 'events_on_map', events: @events %> +
+
+ <%= render 'info', map: @map %> +
+
diff --git a/app/views/clubsite/pages/admin.html.erb b/app/views/clubsite/pages/admin.html.erb new file mode 100644 index 0000000..ae9ebae --- /dev/null +++ b/app/views/clubsite/pages/admin.html.erb @@ -0,0 +1,38 @@ +<% content_for :title, 'Admin' %> + + +
+
+

Users

+ Grant privileges

+ <% if @allow_show_duplicates %> + Show duplicates

+ <% end %> +
+ +
+

Officials

+ Officials certification +
+
+

Series

+ Define +
+ + + +
diff --git a/app/views/clubsite/pages/contact.html.erb b/app/views/clubsite/pages/contact.html.erb new file mode 100644 index 0000000..39d63a7 --- /dev/null +++ b/app/views/clubsite/pages/contact.html.erb @@ -0,0 +1,6 @@ +<% content_for :title, 'Contact' %> + + +<%= render_content_blocks 'contact' %> diff --git a/app/views/clubsite/pages/display.html.erb b/app/views/clubsite/pages/display.html.erb new file mode 100644 index 0000000..a3353b0 --- /dev/null +++ b/app/views/clubsite/pages/display.html.erb @@ -0,0 +1,21 @@ +<% content_for :title, @page.name %> +<% can_edit = user_signed_in? && current_user.has_privilege?(Settings.privileges.page.edit, current_club) %> +<% can_delete = user_signed_in? && current_user.has_privilege?(Settings.privileges.page.delete, current_club) %> + + + +
+ <%= raw @page.content %> +
diff --git a/app/views/clubsite/pages/export.html.erb b/app/views/clubsite/pages/export.html.erb new file mode 100644 index 0000000..5a8fdd2 --- /dev/null +++ b/app/views/clubsite/pages/export.html.erb @@ -0,0 +1,39 @@ +<% content_for :title, 'Export' %> + +
+
+
+

Display info on your club's website

+
+ Much of the information in the OCNDB can be accessed and displayed in your club's website. + A number of "widgets" have been developed for displaying information. A little bit of basic web-programming knowledge is required to include them. + +
Calendar
+ The events calendar can be embeded by adding this code to your site: +
+ + <iframe src="<%= current_club.acronym.to_s.downcase %>.whyjustrun.ca/events.embed"></iframe> + + +
Maps
+ Maps can be embeded by adding this code to your site: +
+ + <iframe src="<%= current_club.acronym.to_s.downcase %>.whyjustrun.ca/maps.embed"></iframe> + +
+
+

Roll-your-own widgets

+

A lot of the information in the OCNDB is available via the API, which generally provides data in IOF XML format.

+

Experienced web developers can create widgets to display this information. For more information check out the API.

+
+
+
+

Upgrading to the WhyJustRun system

+
+ In addition to this functionality, each club has the option to use the WhyJustRun (WJR) system for their club. This involves migrating the club website to WJR... +
+
diff --git a/app/views/clubsite/pages/home.html.erb b/app/views/clubsite/pages/home.html.erb index 3527ec9..2630b5e 100644 --- a/app/views/clubsite/pages/home.html.erb +++ b/app/views/clubsite/pages/home.html.erb @@ -1,5 +1,22 @@ -
-
-

<%= current_club.name %>

-
+
+
+
+

Events

+
+ <%= render 'clubsite/events/list' %> +
+ +
+
+

News

+
+ <% if current_club.juicer_feed_id.blank? && user_signed_in? && current_user.has_privilege?(Settings.privileges.club.edit, current_club) %> + Add news source + <% end %> + <%= render 'clubsite/shared/juicer_feed' %> + <%= render 'clubsite/shared/facebook_like' %> +
+
+ <%= render_content_blocks 'general_information' %> +
diff --git a/app/views/clubsite/pages/other.html.erb b/app/views/clubsite/pages/other.html.erb new file mode 100644 index 0000000..8a2b0d0 --- /dev/null +++ b/app/views/clubsite/pages/other.html.erb @@ -0,0 +1,43 @@ + +
+
+

Events

+ You can enter your club's events in the OCNDB quickly and easily. This allows you to: +
    +
  • do event registration
  • +
  • post results
  • +
  • post course maps
  • +
  • expose your event to other clubs
  • +
+ +

Maps

+ You can keep track of your club's maps in the OCNDB. You cann define each map, including scale, mapping standard, and attributes such as date created, last updated, etc . You can also + upload images of the map and specify the map location. The map can be linked to events so that competitors can see the image and see where the map is located. There is also an + overview that shows the location of all the club's maps. + +

Officials

+ Clubs are strongly encouraged to track Officials' certification levels in the OCNDB. This information is useful in many ways, including tracking numbers of qualified officials, recording + pre-requisites for official certification, and identifying qualified officials for major events. + +
+
+

Export

+ This page show how you can incorporate the information from the database into your club's website. + +

Reports

+ Here you can see basic reporting on club information such as participation counts and officials lists, along with examples of "widgets" you can include in you club's website (see below + for more info). + +

Admin

+ Use this page to seet up other users who will be allowed to enter and make changes to your club's information. +
+
+

Upcoming events

+ <%= render 'clubsite/events/list' %> +
+
diff --git a/app/views/clubsite/pages/resources.html.erb b/app/views/clubsite/pages/resources.html.erb new file mode 100644 index 0000000..1bf8344 --- /dev/null +++ b/app/views/clubsite/pages/resources.html.erb @@ -0,0 +1,31 @@ +<% content_for :title, 'Resources' %> + + +<% if @pages.empty? %> +

Sorry, no resources are posted yet.

+<% end %> +<% @pages.each do |page| %> +

<%= link_to page.name, "/pages/#{page.id}" %>

+<% end %> + +<% if user_signed_in? && current_user.has_privilege?(Settings.privileges.page.edit, current_club) %> +
+

Add page

+ <%= form_with scope: :page, url: '/pages/add', html: { class: 'form-horizontal' } do |f| %> +
+ <%= f.label :name, 'Name', class: 'control-label col-sm-2' %> +
+ <%= f.text_field :name, class: 'form-control', 'data-validate' => 'validate(required)' %> +
+
+
+ <%= f.label :content, 'Content', class: 'control-label col-sm-2' %> +
+ <%= f.text_area :content, class: 'wjr-wysiwyg', rows: 20, 'data-validate' => 'validate(required)' %> +
+
+ <%= f.submit 'Add', class: 'btn btn-primary' %> + <% end %> +<% end %> diff --git a/app/views/clubsite/results/index.html.erb b/app/views/clubsite/results/index.html.erb new file mode 100644 index 0000000..00186d9 --- /dev/null +++ b/app/views/clubsite/results/index.html.erb @@ -0,0 +1,44 @@ + + +
+ + + + + + + + + + + + + <% @results.each do |result| %> + <% event = result.course.event %> + + + + + + + + + <% end %> + +
DateEventCourseNameTimeStatus
<%= event.local_date.strftime('%b %-d, %Y') %><%= link_to event.name, clubsite_event_path(id: event.id) %><%= link_to result.course.name, clubsite_course_path(id: result.course.id) %><%= result.user&.name %><%= result.time&.strftime('%-H:%M:%S') %><%= result.readable_status %>
+
+ +<% if @total_pages > 1 %> + +<% end %> diff --git a/app/views/clubsite/shared/_facebook_like.html.erb b/app/views/clubsite/shared/_facebook_like.html.erb new file mode 100644 index 0000000..9905c46 --- /dev/null +++ b/app/views/clubsite/shared/_facebook_like.html.erb @@ -0,0 +1,13 @@ +<% if current_club.facebook_page_id.present? %> + +<% end %> diff --git a/app/views/clubsite/shared/_flickr_photos.html.erb b/app/views/clubsite/shared/_flickr_photos.html.erb new file mode 100644 index 0000000..d036d44 --- /dev/null +++ b/app/views/clubsite/shared/_flickr_photos.html.erb @@ -0,0 +1,24 @@ +<%# locals: event_id %> +
,<%= ERB::Util.url_encode("orienteerapp#{event_id}") %>" data-flickr-api-key="<%= Settings.flickrApiKey %>"> +

Photos

+

Photos are from Flickr. To add your photos to this section, tag your Flickr photos with: whyjustrun<%= event_id %> (all one word)

+ +
diff --git a/app/views/clubsite/shared/_juicer_feed.html.erb b/app/views/clubsite/shared/_juicer_feed.html.erb new file mode 100644 index 0000000..807357c --- /dev/null +++ b/app/views/clubsite/shared/_juicer_feed.html.erb @@ -0,0 +1,6 @@ +<% if current_club.juicer_feed_id.present? %> + + +
    +

    Feed powered by Juicer.io

    +<% end %> diff --git a/config/brakeman.ignore b/config/brakeman.ignore index b05315e..931a013 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -15,6 +15,10 @@ { "fingerprint": "dcd469c3af248cfe9ad0bd62ca6d04d009d336c4a057ed702eae420d78ad92f2", "note": "Intentional 301 from a club's old domain to its current one; both values come from the clubs table, not from user input." + }, + { + "fingerprint": "6f385d255ed0c43e612d0bb77992a41e96ac6c9186d2510ca2616f5ac694982d", + "note": "MediaStore builds the path from the configured data folder, the club id, and an Integer-cast record id; no user-controlled path components." } ], "updated": "2026-07-30 00:00:00 +0000", diff --git a/config/initializers/geocoder.rb b/config/initializers/geocoder.rb index 6520d76..0ecad52 100644 --- a/config/initializers/geocoder.rb +++ b/config/initializers/geocoder.rb @@ -1,3 +1,8 @@ -Geocoder::Configuration.lookup = :nominatim -Geocoder::Configuration.cache = {} -Geocoder::Configuration.units = :km +Geocoder.configure( + lookup: :nominatim, + units: :km, + timeout: 5, + # Nominatim's usage policy requires an identifying User-Agent + http_headers: { 'User-Agent' => 'WhyJustRun (support@whyjustrun.ca)' }, + cache: Rails.cache +) diff --git a/config/routes.rb b/config/routes.rb index d430b69..bd7fe2f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -43,6 +43,37 @@ get 'robots.txt', to: 'robots#show', format: false get '/', to: 'pages#home', as: :clubsite_root + # Events + get 'events(/index(/:date))', to: 'events#index', as: :clubsite_events + get 'events/listing', to: 'events#listing' + get 'events/view/:id', to: 'events#show', as: :clubsite_event + get 'events/results/:id', to: 'events#show' + get 'events/map/:id', to: 'events#map' + get 'events/rendering/:id', to: 'events#rendering' + + # Maps + get 'maps(/index)', to: 'maps#index', as: :clubsite_maps + get 'maps/view/:id', to: 'maps#show', as: :clubsite_map + get 'maps/report', to: 'maps#report' + get 'maps/download/:id', to: 'maps#download' + get 'maps/rendering/:id(/:thumbnail)', to: 'maps#rendering' + + # Courses + get 'courses/view/:id', to: 'courses#show', as: :clubsite_course + get 'courses/map/:id(/:thumbnail)', to: 'courses#map' + + # Results + get 'results(/index)', to: 'results#index', as: :clubsite_results + + # Pages and content blocks. The jEditable editors post to the legacy + # CakePHP URLs (/pages/edit, /contentBlocks/edit). + post 'pages/add', to: 'pages#create' + post 'pages/edit', to: 'pages#update' + post 'pages/delete/:id', to: 'pages#destroy' + post 'contentBlocks/edit', to: 'content_blocks#update' + get 'pages/:page', to: 'pages#show', as: :clubsite_page + get 'Pages/:page', to: 'pages#show' + match '*unmatched', to: 'errors#not_found', via: :all, format: false end end diff --git a/public/clubsite-defaults/Course.png b/public/clubsite-defaults/Course.png new file mode 100644 index 0000000000000000000000000000000000000000..8a261f0ce80370ba0d37525fb8f067696bce6409 GIT binary patch literal 3250 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 IPS2D4Zxkln$^ZZW literal 0 HcmV?d00001 diff --git a/public/clubsite-defaults/Map.png b/public/clubsite-defaults/Map.png new file mode 100644 index 0000000000000000000000000000000000000000..dc1e2882b3ef2565a3fb444492bbf9343e72bec7 GIT binary patch literal 3187 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 literal 0 HcmV?d00001 diff --git a/public/img/maps-markercluster/m2.png b/public/img/maps-markercluster/m2.png new file mode 100644 index 0000000000000000000000000000000000000000..b999cbcf69441f194768157d79a113b0fae03e3c GIT binary patch literal 3259 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 O0000KLZ*U+=)p!fv7f#TG` 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 literal 0 HcmV?d00001 diff --git a/public/img/maps-markercluster/m5.png b/public/img/maps-markercluster/m5.png new file mode 100644 index 0000000000000000000000000000000000000000..61387d2ab5c8d22efef4846617567a3b1ae6d72e GIT binary patch literal 6839 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 literal 0 HcmV?d00001 diff --git a/public/img/orienteering_symbol.png b/public/img/orienteering_symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..2e2ac892c1b5d9b8bc329eaf039b109cb0352b55 GIT binary patch literal 7274 zcmaJ`eLPfY`=3esQ>|#XcI~sROeEQ2!XOidmDIzbR;m$MF&G--WxN~IdV1ZW6g6Zj zjPhDjBE_iIB1JhFd7WOoG=`8thB3eUoHN?zkKg|K=;OZcbG>}8>%Q;rxz6R}E*Hn; z%YIyj!{L_i+_CLf91i~p{2yJi2&@nuJbes){fp(WpXJ8%XGQyj({Ofv%tJK7PKHka z?N^$QUu;+-%?gK8Sx4WqpS2%yCi^lOMm~s)Q4Av#Xyb5Jl$cN--(VVxaEKN_53$x6 zeIU>w(EY4+_L)JOp-_8TAbm$%IL$52Wsh%Mu&EGpN1p$#D>y{ztSR@k>S2z#c~|(hwh!*?DoWT4)@G5_fB3P93Q+ilRLn?mh5;! zcYD3lA1_P(8V)4==;#~hbnbNCiCv3+{m$;IRrs&^N^Q>_IBjUiFkpzQC1fvWlL&wA zh)s8Fji}}zg>=AZ1a9e!9n6|6zL2!BiT^*gKeb-2{{y$;q#v1DJuK+J6Eb)d_iTlO z5Pv;TR;Yja>*E9S1;r{UBB^V}_nJv^ib79Kunr*#U?D?yw zjZ8ZU65JW} zNFdIoPh9V!SHWc49t-xTy>MAn_W(EAL-ocWFEtN}e!(h;OeTlO{tRt>SUGGR?U>{w z#1EB=p|6{2CZc7UG_9-i9GgU4deDY4uZO1v8zs0&q4Zkb?Q6pVR&zH8|k7w{II@7SAvUdl^zSfCF!MHELO1^;-yaQ zg9M5FH3{Za)tEd;TU^AkdBqmkTmw(5cpB4tA;HTCZ3;-ORUywC%CLCGekkDX72?-8 zz)b0$LFwDx+4;*ftv9<#?3ttCd82j-;lNTQq&?*F;~*z-1kX+c^Ao^He$})|r{Yab z3b0ko(};411eRmAf>Ye3Dr2FX-kBAgk9XC_c5!S%@5^F|zQ1d{4Yn*%vHHOCE^HQS zDdZ>ca8)42k=;?CC z=5hO*UUIEGNOpNBdwxe2fECK+^s=kS1+J;qpZ4hh?QSV0$!V|E!K^i9$2H%tSF5cy zlRs^g(W{D8x(~n)T-F)7zH)_wLmuP0+3?m>{f(Kg{MQc+xi^w)d(7nf!MQWvN@WtA z0kDVS%v7*j9XH7djE^NM-E=s$8+d9PY>MzutxV^6Z@Ew$$IozXAKAzP3nM(PBELBc zgiz5VrKqnf;ucQu+PhQml_flG7q>XJDx>+8!U?3Ki!S4PkXQd%1jb1n(z9%L_|Ccu zn&7M_^+@l;7sV4}e|!-gVIUq^x^3+97Z-IB=hnAj*AH2K(CHKD#7Muu&D9#6ymY zq0GqS0}!Rg+DmZ=op>Y;7fJ7Az6%6tny3*wcEQVujxaDc5DljucA_Nd@qnkzsac6J z!ByjJ4V;4$$-0}pvugBuq#h-k-pvZLg-2%ErS@4clysU8AU+fDv=2LV9W1*@D;8Y^ zR&Ryj%(xwW?ElYci<7TdJy5)l#!Y~=4sK7j1i&IQV9gsiTviEF^j zpTt?ZmvFw>3faKh7V`SGj?B^LMNF#jNK95ufcwcTgpC9ii{Vpmkl5mR0R1mtVTh;mM+g?h`L@iUr{NIaJc9Y~y(|qlH zn0W7e$i3(iEkCU#-ciJ^oUe+)I`mIwy5jgA2le5h#XKSA@Wq+U+ckqg4#YsOYH7DKbX1uY;y(j&` z-li#GZZb5Q4+1cC0%dzz`FAT5{sdlAm{C7d@8?w}yh0$nkT&MgcD{^#hCcDXPA-UE zgXHrqxb2+Y$j2DW4E<9Z^$pR~dXLA94>A#$G5ym0oewKN*f5q+RUb2Dubvl*tpl9L zm&`0Zi!e%H=ffR&t2+~Szxw=)0?@;dspuVV+Z-fI<+xxGj`=Z1Wh0( z(Tv)=dHN-$zuLMjKJQ6258Zj9_pY;2C6ZTO*3%~8&oL>LZ2ElxpSmTI|ASsPcd_#>Yb=bZ0rV(eIi_z?d*|r14_-(2?==&h#{_hDFj~7 zhv;#G<9_|;JD;T(fLm}?Tz@`8ue@U?rbN^(X%^pc<6WWvL?^Nm`-GNWNY+3=z+XP#9@p12_%oYD`@Da|)UaG&K7 zi}LSa5kwCcadPx{w!*b@JZ{f{p7=&Lw_Ot%28%^fQ%WG2uYQ{(m*)75m~ze*+pB#N zMeA{Gio^|M;J{6kFUf&fep>GWWfgSE1gDwOzw?JBC1U+(UJ3ElsqR%wCMbwdG(dHhww9deVfLUj{8(9`9x0T{B`~p9 z=>nx;Gy*|N{a+k6u+0|EnupmKtEpWn9>+d zM!<>6b!G?bF>wL2IV!;q6Nx_4(VRMhi69)T9FFgvV&B0GiV`l3c=%w_QyTo9jhKi+ zCO_Qrv(jJ!aW{DpSWSTWi~<(0GxM8422Dx#!a zG#wc2q6nCPb??K#YYMTYkjIq?zkO9u?~IFXm690jgK(I~@%@~dkN4-My)uBwV?2GX z&4KJ*-=(OX&VW;Lj4ZL1j&zZ*V?z^vJ{Q@Cj(d5Q|w$MWyE!!08fi}B;hx^wPu z07hmG&U;x0I!kzd!Ziy%n2yt2>J?^@iD@qdx7pVr@` zYnL#0gE-&=JHi7FoU<@;D;f_#f=-`h*@zENd?TxXBTv4~iw*0DZ(fCN67N4WQLGz? zI1#RAu7j5Mm!C#%;8C82-kIezg`VBQPzv@{)PRd$9kJCZq zEhQ@H!yDI`xfbk!d7K$xYo$-oVm9r_Yr7n8=^V^K`dl7{noeycSZW>SutmS&urI$NRl_ zr30+kl2)AfZ}_bpzAmQxj4hZ zOl#;&h4cL)-l#V>aXbUlnJ(vb^$L-g(&TrIf|DGvIO5wuq1lP|y8?>U_d~-?)5AEv z3*g&3KhMh8TS3Ac;OMev_mX~7MeUb<*I}Wp6lX`=d#{U$2!fwQB_75^no|e)A=vP2 zDt|O<1U0A_cyhRX875tkls0mZj)}C72E^~}K>aJLUvTigRKJTGz`l`+q>VzV0 zxAMyF30|5O`8ZRrkwbH+w{gPp*VM4?1>bAB-R%>wA5Dk#6sB>2?7({KxI$FAAMrF`wIAYJ+LTk zatun{2X3biSk4WMYg$=%F+Bt4XAM|5=HcfF3k1!>@VHIj`~OSU`s(E+Y(ZZ*uYRzq!}y zr<|17t4QfvOSi^K-`*xr`l7+3r7x0`FQqT48?0HB^dMyJ80LgMSd`=&cj)!1+T3@& zGI?;^s3oMDE!5pDjmm>gH#L5PBn%X>QF+f#KW*HN&ElJR&rdfQK&nGW<0aFtrKWA5 zI>i@E{;<%;xQuLS*8Cu|@l<^<{GNv!I+e9Kx=p%K-%KlQazFUY(V@rw0w0m}t$(Iv zUOf@R58uh~e++_id?EGS*>t{hI1j^zbuSFuaFfrfC%X|7T+-pl6-t^>E= zW<5CopD7C>uecp6P4-QUajFuRdr&Kfl3{RSDR(V=x3z|R@^ykWMc!+r6I zwGzPE{txz6rr?bIth?sz?#1UzR9Dyw@1Tu{@NwLshPbKz_>R;fFH1zTT#z;5CE?>! zxRd;6oubA>k3kMPajvMoG8!ED2=}-}<_Qa8Hi`tjZHtyrBoUBEHa%DSKbgx2bHy!Y z?53=lmepSR$zykH+NIB#eX3oa>#)ha_6hqeueD%u@O$ur`}E|1=Ne?vNAL24E2_+K zZv)t+7%4~iV17X7h|>A75C`DgS9T?w7OhAW4szx@oMy&HN>yz?Q@O!dW5QDn^CQ&! zl)lw#DVXF0xWle&i3zTom}*_cD?q-N)K}Eefg8zu!2E28CJuler1PgPzk8-3R7Jz_ zo?wMVm5K3>TjDa-1!sJxH9M>hT*4iN;CpVh)|*GM`&}RB0a9MkM7LIwayNz@tBr#( z81Am$YpVb2M>H$)^!&Oy?-Dg&C;es~jUG00;v{e&8{Px1U)anIHafxD*8ZM|zLKK9 z2kNdEMeoJTqU{OBGBgP_@5bM?h}4#?NDwxOZ{Lj#d&%zP;2s53XID|pst5ltzz;pL zmM^Xk4mKD1qC?}hyJZ>NLHw$fjIJNmx>TV;#@ literal 0 HcmV?d00001 diff --git a/public/img/social/attackpoint-logo.png b/public/img/social/attackpoint-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ea41af0fbb4a680bf134663a1be2bff1a877050f GIT binary patch literal 1496 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 DWv&`i literal 0 HcmV?d00001 diff --git a/public/img/social/facebook-logo@2x.png b/public/img/social/facebook-logo@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..593ed9d58e8b49fcc7d96858bfefa49f9577af05 GIT binary patch literal 1641 zcmaJ>drT8|9Isfd8dN2AqsnWYvDWRp50yXNij{|PdyLvsOAk0lS z=6sOI%(OrYxT(Mdbr2skfVe-_Q5^`M&NO zy>9u8>2s$G1cDiwNR@$?A^vw-0RQy?k!yJIKBqQuaZD1IVP;8z(#BXwKw~#2lLpdk z%UpMc3>OFjQYfQ|GijqSD`OX#{W>C--NB;;g7Bp-huNA+a)5D4Rl< zDAqz+2SKJ#ky$Jmm!&gWvr?^cn{eq8Al!xV2J9qf23+%AQp*3tq>drNaZ3}42Q|!j{yXNQ4m5w7!8FH43cB87#M$qyf@aC zgc(%J#(nWSg)oKV92f{@WMqgkP!Yo>gRopK2O$JRkWgMD)R{?hW>+ZfoIhbfMLMl4 z<=`lW2K*My7ABoj2zjP2L$Euv+E<2Y=Xj#{lz}d@1B68oXt(?08q;=i2J&ATlWIGS znGO;(kWMC@wes~ynm+;NbN6~jenp-Q7Q<3}QOs#7#+q&?X-=b32>Bb4jj~~oSO&qc zGz^j=kVFQV~g$W!|N>#WTS1yxI zU^TRpGt*Xb!Y{@99mgWC#bN|YnmL9wGECY;2k286j&Y_i4uFW003lklm7@Izi~I$8 z8Lx_Dsr9678Ozv#u?%C>Bn)tzcTOmkN(n6C*&{Mst;7j6AC8K^Q8^?W$J!>z+Bg>f zUo6Oz0sZaqpLUrz;(O4)eAWE?=GFL+G(R&eKO8#tYP>)&MW&B7suw4E7AJZ~M@JVY z7T46@fOLCc-5yZ8=R{K{mf(@c7kLlbmQO)N*`vX)uw$fXHuZxg|CfG`ICg;tJhmEi$7=wPx?_KORS{ zEEySjAu|>gSDw3h`{99_OB;8dic2l8JaL8FQX6ltK=p1c(QC9<@^;8Cpb zp1sg}<7WS{hK@Y%>Aw4e8}jNhx1Wr%?c3pLE~z@NSW%qvSxrZGza*}xsinL3?&H#` z^I}6`SI@(~dxO#BzKB&N7q8t#^lm+M;IE-)VX*~pOhNh4%UwMKrN^5@d=BF@Dy7kN z{lTs)>o-F|xuMh61=%8~LJ3t}JKVKl;N)=sNWHTB?cfiQ?!nsHbB}h`&40Qau_e#h zVi&L4AAu|giu`2S=Rva?zY4j7343OqE~}_m*)9#EmXwe)N9a^`+#+MXr3&l;jg94;w}nq3kZJ^bj$sa?-EU9NoZ>zOrf z->*aPkvUmSM}GePSl`a_>{GT~=`-766Rb$omz!tAhQw@XIFq7&BlUSe^WNUKMkT@d zyN*g&=mNe-d*@==9NGD>%CLrpZTUIvxikz%*X_=3CwQ4R((vNVg}KVvF4|%_Sh01r zRUIAl?e3hLpZdPg1vaOzF7E1$&Td*Vuz4-=V&Lz7MOD2v*N06#cxWgf;-kQ{n3iLT x(uR%j6id?Kt*vdoCzf}9fMx|2t$FtDT0x*-UT+Lr_SFCL*Qj->Dtzsxe*k{^jO73T literal 0 HcmV?d00001 diff --git a/test/fixtures/content_blocks.yml b/test/fixtures/content_blocks.yml new file mode 100644 index 0000000..4888691 --- /dev/null +++ b/test/fixtures/content_blocks.yml @@ -0,0 +1,17 @@ +cluba_general_information: + key: general_information + order: 1 + content:

    Welcome to Club A!

    + club: cluba + +cluba_contact: + key: contact + order: 1 + content:

    Contact Club A at cluba@example.com.

    + club: cluba + +clubb_general_information: + key: general_information + order: 1 + content:

    Welcome to Club B!

    + club: clubb diff --git a/test/fixtures/courses.yml b/test/fixtures/courses.yml new file mode 100644 index 0000000..c87c976 --- /dev/null +++ b/test/fixtures/courses.yml @@ -0,0 +1,11 @@ +race_long: + event: cluba_race + name: Long Course + distance: 5000 + climb: 150 + description: The long course + +race_score: + event: cluba_race + name: Score Course + is_score_o: true diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml index bba961d..5ee5070 100644 --- a/test/fixtures/events.yml +++ b/test/fixtures/events.yml @@ -2,6 +2,7 @@ cluba_race: name: Spring Sprint club: cluba event_classification: local + series: cluba_wednesday date: 2025-06-15 17:00:00 finish_date: 2025-06-15 19:00:00 lat: 49.3 @@ -12,3 +13,10 @@ cluba_training: club: cluba event_classification: club date: 2025-07-02 01:30:00 + +cluba_external: + name: External Championships + club: cluba + event_classification: regional + date: 2025-08-10 16:00:00 + custom_url: https://example.com/external-event diff --git a/test/fixtures/maps.yml b/test/fixtures/maps.yml new file mode 100644 index 0000000..6cbb552 --- /dev/null +++ b/test/fixtures/maps.yml @@ -0,0 +1,6 @@ +cluba_forest: + name: Forest Map + club: cluba + scale: 10000 + lat: 49.31 + lng: -123.11 diff --git a/test/fixtures/pages.yml b/test/fixtures/pages.yml new file mode 100644 index 0000000..fc62bbd --- /dev/null +++ b/test/fixtures/pages.yml @@ -0,0 +1,17 @@ +cluba_trail_guide: + name: Trail Guide + section: Resources + content:

    Club A trails and how to find them.

    + club: cluba + +cluba_membership_info: + name: Membership Info + section: Resources + content:

    How to join Club A.

    + club: cluba + +clubb_handbook: + name: Club B Handbook + section: Resources + content:

    The Club B handbook.

    + club: clubb diff --git a/test/fixtures/results.yml b/test/fixtures/results.yml new file mode 100644 index 0000000..a2aa989 --- /dev/null +++ b/test/fixtures/results.yml @@ -0,0 +1,12 @@ +member_long: + user: member + course: race_long + status: ok + time_seconds: 3600 + registrant: member + +organizer_long: + user: organizer + course: race_long + status: did_not_finish + registrant: member diff --git a/test/fixtures/series.yml b/test/fixtures/series.yml new file mode 100644 index 0000000..a0900fd --- /dev/null +++ b/test/fixtures/series.yml @@ -0,0 +1,6 @@ +cluba_wednesday: + name: Wednesday Evening Series + acronym: WES + color: '#ff6600' + is_current: true + club: cluba diff --git a/test/integration/clubsite_events_test.rb b/test/integration/clubsite_events_test.rb new file mode 100644 index 0000000..40b663b --- /dev/null +++ b/test/integration/clubsite_events_test.rb @@ -0,0 +1,137 @@ +require 'test_helper' + +class ClubsiteEventsTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + setup do + host! 'cluba.test' + end + + test "index renders the calendar seeded with today's date" do + get '/events/index' + assert_response :success + today = Time.now.utc.in_time_zone('America/Vancouver').to_date + assert_select '.wjr-calendar[data-calendar-year=?]', today.year.to_s + assert_select '.wjr-calendar[data-calendar-month=?]', (today.month - 1).to_s + assert_select '.wjr-calendar[data-calendar-day=?]', today.day.to_s + end + + test "index parses the day-first date param" do + get '/events/index/15-06-2025' + assert_response :success + assert_select '.wjr-calendar[data-calendar-year="2025"][data-calendar-month="5"][data-calendar-day="15"]' + end + + test "index shows a legend of current series" do + get '/events/index' + assert_response :success + assert_select "ul.series-legend li.series-#{series(:cluba_wednesday).id}", text: 'Wednesday Evening Series' + end + + test "listing renders the knockout event list fed by the IOF XML feed" do + get '/events/listing' + assert_response :success + assert_select '.event-list[data-event-list-url=?]', + "#{Settings.coreURL.chomp('/')}/club/#{clubs(:cluba).id}/events.xml?iof_version=3.0&external_significant_events=all" + assert_select 'script#event-box-template' + end + + test "show renders courses and registrations in the club's local time" do + event = events(:cluba_race) + travel_to Time.utc(2025, 6, 1, 12) do + get "/events/view/#{event.id}" + end + assert_response :success + assert_select 'h1', text: /Spring Sprint/ + # 17:00/19:00 UTC render as Pacific local times + assert_select 'h2.event-header', text: 'June 15th 2025 10:00am - 12:00pm' + assert_select 'h3', text: 'Long Course' + assert_select 'h3', text: 'Score Course' + assert_select 'a', text: 'Mary Member' + assert_select 'a', text: 'Oscar Organizer' + end + + test "show renders the results section once the event has results posted" do + event = events(:cluba_race) + event.update_column(:results_posted, true) + get "/events/view/#{event.id}" + assert_response :success + assert_select 'h2', text: 'Results' + assert_select '.result-list[data-result-list-url=?]', + "#{Settings.coreURL.chomp('/')}/iof/3.0/events/#{event.id}/result_list.xml" + assert_select 'h2', text: 'Course Maps' + end + + test "results path is an alias for show" do + event = events(:cluba_race) + get "/events/results/#{event.id}" + assert_response :success + assert_select 'h1', text: /Spring Sprint/ + end + + test "show as json includes the event's associations and status flags" do + event = events(:cluba_race) + get "/events/view/#{event.id}.json" + assert_response :success + body = JSON.parse(response.body) + assert_equal 'Spring Sprint', body['name'] + assert_equal 'Wednesday Evening Series', body['series']['name'] + assert_equal %w[Long\ Course Score\ Course], body['courses'].map { |course| course['name'] }.sort + assert_equal 2, body['courses'].sum { |course| course['results'].length } + assert body['completed'] + assert_not body['registration_open'] + end + + test "show as xml permanently redirects to the IOF result list" do + event = events(:cluba_race) + get "/events/view/#{event.id}.xml" + assert_response :moved_permanently + assert_redirected_to "/iof/3.0/events/#{event.id}/result_list.xml" + end + + test "an event with a custom url redirects anonymous visitors" do + get "/events/view/#{events(:cluba_external).id}" + assert_redirected_to 'https://example.com/external-event' + end + + test "an event with a custom url renders with a notice for its organizer" do + event = events(:cluba_external) + Organizer.create!(event: event, user: users(:organizer), role: roles(:organizer)) + sign_in users(:organizer) + get "/events/view/#{event.id}" + assert_response :success + assert_includes response.body, 'You are seeing this page because you can edit this event.' + assert_select 'a[href=?]', 'https://example.com/external-event', text: 'Continue to Event' + end + + test "rendering 404s when no results file has been uploaded" do + get "/events/rendering/#{events(:cluba_race).id}" + assert_response :not_found + end + + test "rendering 404s for a non-numeric id" do + get '/events/rendering/nope' + assert_response :not_found + end + + test "another club's domain cannot show this club's event" do + host! 'clubb.test' + get "/events/view/#{events(:cluba_race).id}" + assert_response :not_found + end + + test "embed format renders the calendar without the navbar" do + get '/events/index.embed' + assert_response :success + assert_select '.wjr-calendar' + assert_select 'nav', count: 0 + end + + test "map renders the marker map in the embed layout" do + event = events(:cluba_race) + get "/events/map/#{event.id}" + assert_response :success + assert_select '.simple-marker-map[data-lat=?]', event.lat.to_s + assert_select 'nav', count: 0 + end +end diff --git a/test/integration/clubsite_maps_test.rb b/test/integration/clubsite_maps_test.rb new file mode 100644 index 0000000..3c4ffab --- /dev/null +++ b/test/integration/clubsite_maps_test.rb @@ -0,0 +1,154 @@ +require 'test_helper' +require 'geocoder/lookups/test' + +class ClubsiteMapsTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + setup do + host! 'cluba.test' + Geocoder.configure(lookup: :test) + Geocoder::Lookup::Test.set_default_stub( + [{ 'latitude' => 49.31, 'longitude' => -123.11, 'address' => { 'suburb' => 'Mapville', 'city' => 'Vancouver' } }] + ) + end + + teardown do + Geocoder.configure(lookup: :nominatim) + end + + test 'maps index renders the multi-marker map without edit affordances for visitors' do + get '/maps' + assert_response :success + assert_select 'h1', 'Maps' + assert_select '.multi-marker-map[data-fetch-url="/api/maps.json"]' + assert_select 'a[href="/maps/edit"]', count: 0 + end + + test 'maps index shows edit affordances for users with the maps edit privilege' do + sign_in users(:executive) + get '/maps/index' + assert_response :success + assert_select 'a[href="/maps/edit"]' + end + + test 'maps index embed variant renders with the embed layout' do + get '/maps/index.embed' + assert_response :success + assert_select '.multi-marker-map' + assert_select 'link[href*="clubsite_embed"]' + assert_select 'nav.navbar', count: 0 + end + + test 'maps show renders the map details' do + map = maps(:cluba_forest) + # update_columns: the shared event fixture doesn't satisfy the model's + # required series/map associations + events(:cluba_race).update_columns(map_id: map.id) + get "/maps/view/#{map.id}" + assert_response :success + assert_select 'h1', /Forest Map/ + assert_select 'td', '1:10,000' + assert_select 'td', text: /Spring Sprint/ + assert_select 'p', 'Mapville, Vancouver' + end + + test 'maps report lists the maps sorted by name' do + get '/maps/report' + assert_response :success + assert_select 'h1', 'List of maps' + assert_select 'a[href=?]', "/maps/view/#{maps(:cluba_forest).id}", text: 'Forest Map' + end + + test 'maps download redirects to the file url' do + map = maps(:cluba_forest) + map.update!(file_url: 'https://example.com/forest.ocd') + get "/maps/download/#{map.id}" + assert_redirected_to 'https://example.com/forest.ocd' + end + + test 'maps download rewrites Dropbox links to direct downloads' do + map = maps(:cluba_forest) + map.update!(file_url: 'https://dl.dropboxusercontent.com/s/abc123/forest.ocd') + get "/maps/download/#{map.id}" + assert_redirected_to 'https://dl.dropboxusercontent.com/s/abc123/forest.ocd?dl=1' + end + + test 'maps download leaves Dropbox links with a query string untouched' do + map = maps(:cluba_forest) + map.update!(file_url: 'https://dl.dropboxusercontent.com/s/abc123/forest.ocd?dl=0') + get "/maps/download/#{map.id}" + assert_redirected_to 'https://dl.dropboxusercontent.com/s/abc123/forest.ocd?dl=0' + end + + test 'maps download 404s when the map has no file url' do + get "/maps/download/#{maps(:cluba_forest).id}" + assert_response :not_found + end + + test 'maps rendering serves the default image when no upload exists' do + get "/maps/rendering/#{maps(:cluba_forest).id}" + assert_response :success + assert_equal 'image/png', response.media_type + end + + test 'maps rendering 404s for an invalid thumbnail size' do + get "/maps/rendering/#{maps(:cluba_forest).id}/999x999" + assert_response :not_found + end + + test 'maps rendering serves the default image for a valid thumbnail with no upload' do + get "/maps/rendering/#{maps(:cluba_forest).id}/400x600" + assert_response :success + assert_equal 'image/png', response.media_type + end + + test 'courses map serves the default image when no upload exists' do + get "/courses/map/#{courses(:race_long).id}" + assert_response :success + assert_equal 'image/png', response.media_type + end + + test 'courses show renders the course with its results' do + course = courses(:race_long) + get "/courses/view/#{course.id}" + assert_response :success + assert_select 'h1', /Long Course/ + assert_select 'td', text: 'Mary Member' + assert_select 'td', text: '1:00:00' + assert_select 'td', text: 'DNF' + end + + test 'results index renders the results for the club' do + get '/results' + assert_response :success + assert_select 'h1', 'Results' + assert_select 'td', text: 'Mary Member' + assert_select 'td', text: /Long Course/ + end + + test 'results index paginates with page param' do + get '/results/index', params: { page: 2 } + assert_response :success + assert_select 'td', text: 'Mary Member', count: 0 + end + + test 'maps from another club 404 on this domain' do + host! 'clubb.test' + get "/maps/view/#{maps(:cluba_forest).id}" + assert_response :not_found + end + + test 'map downloads from another club 404 on this domain' do + map = maps(:cluba_forest) + map.update!(file_url: 'https://example.com/forest.ocd') + host! 'clubb.test' + get "/maps/download/#{map.id}" + assert_response :not_found + end + + test 'courses from another club 404 on this domain' do + host! 'clubb.test' + get "/courses/view/#{courses(:race_long).id}" + assert_response :not_found + end +end diff --git a/test/integration/clubsite_pages_test.rb b/test/integration/clubsite_pages_test.rb new file mode 100644 index 0000000..400ec2f --- /dev/null +++ b/test/integration/clubsite_pages_test.rb @@ -0,0 +1,199 @@ +require 'test_helper' + +class ClubsitePagesTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + # Only the tables this feature touches, so the test is independent of + # fixtures belonging to other features. + self.fixture_table_names = %w[club_categories clubs groups users privileges pages content_blocks] + + setup do + host! 'cluba.test' + end + + test "home renders the club's content blocks" do + get '/' + assert_response :success + block = content_blocks(:cluba_general_information) + assert_select "div#content-block-#{block.id}.content-block", text: 'Welcome to Club A!' + end + + test "home content blocks are not editable for signed out visitors" do + get '/' + assert_response :success + assert_select '.content-block.wjr-editable', count: 0 + end + + test "home content blocks are editable for privileged users" do + sign_in users(:executive) + get '/' + assert_response :success + block = content_blocks(:cluba_general_information) + assert_select "div#content-block-#{block.id}.content-block.wjr-editable" + end + + test "club with the other layout renders the other home template" do + host! 'clubb.test' + get '/' + assert_response :success + assert_select 'h1', 'Orienteering Canada Database' + end + + test 'contact renders its content blocks' do + get '/pages/contact' + assert_response :success + assert_select 'h1', 'Contact Us' + block = content_blocks(:cluba_contact) + assert_select "div#content-block-#{block.id}.content-block", text: 'Contact Club A at cluba@example.com.' + end + + test 'contact auto-seeds default content blocks on first read' do + host! 'clubb.test' + assert_difference -> { clubs(:clubb).content_blocks.where(key: 'contact').count }, 1 do + get '/pages/contact' + end + assert_response :success + assert_select '.content-block', text: 'No contact information has been entered yet.' + end + + test "resources lists the club's resource pages" do + get '/pages/resources' + assert_response :success + page = pages(:cluba_trail_guide) + assert_select "a[href=?]", "/pages/#{page.id}", text: 'Trail Guide' + # Pages of other clubs are not listed + assert_select 'a', text: 'Club B Handbook', count: 0 + # No add form for signed out visitors + assert_select 'form[action="/pages/add"]', count: 0 + end + + test 'resources shows the add page form to editors' do + sign_in users(:executive) + get '/pages/resources' + assert_response :success + assert_select 'form[action="/pages/add"]' + end + + test 'export renders' do + get '/pages/export' + assert_response :success + assert_select 'code', text: /cluba\.whyjustrun\.ca\/events\.embed/ + end + + test 'admin redirects users without the admin privilege' do + sign_in users(:executive) + get '/pages/admin' + assert_response :redirect + assert_redirected_to '/' + end + + test 'admin redirects signed out visitors' do + get '/pages/admin' + assert_response :redirect + end + + test 'admin renders for the webmaster' do + sign_in users(:webmaster) + get '/pages/admin' + assert_response :success + assert_select 'h1', 'Admin tools' + assert_select 'a[href=?]', Settings.coreURL.chomp('/') + "/club/#{clubs(:cluba).id}/participation_report.csv" + # Webmaster (90) has the user merge privilege (90) + assert_select 'a[href="/users/showDuplicates/"]' + end + + test 'a dynamic page renders its title and content' do + page = pages(:cluba_trail_guide) + get "/pages/#{page.id}" + assert_response :success + assert_select "h1#page-resource-title-#{page.id}", text: 'Trail Guide' + assert_select "div#page-resource-#{page.id} p", text: 'Club A trails and how to find them.' + end + + test 'an unknown page slug 404s' do + get '/pages/nonsense' + assert_response :not_found + end + + test "another club's page id 404s" do + host! 'clubb.test' + get "/pages/#{pages(:cluba_trail_guide).id}" + assert_response :not_found + end + + test 'a privileged user can edit a content block' do + sign_in users(:executive) + block = content_blocks(:cluba_general_information) + post '/contentBlocks/edit', params: { id: "content-block-#{block.id}", value: '

    Updated welcome!

    ' } + assert_response :success + assert_equal '

    Updated welcome!

    ', block.reload.content + assert_equal '

    Updated welcome!

    ', response.body + end + + test 'an unprivileged user cannot edit a content block' do + sign_in users(:member) + block = content_blocks(:cluba_general_information) + original_content = block.content + post '/contentBlocks/edit', params: { id: "content-block-#{block.id}", value: 'hacked' } + assert_response :redirect + assert_equal original_content, block.reload.content + end + + test "a content block of another club cannot be edited through this club's site" do + sign_in users(:executive) + block = content_blocks(:clubb_general_information) + original_content = block.content + post '/contentBlocks/edit', params: { id: "content-block-#{block.id}", value: 'hacked' } + assert_response :not_found + assert_equal original_content, block.reload.content + end + + test 'a privileged user can edit page content' do + sign_in users(:executive) + page = pages(:cluba_trail_guide) + post '/pages/edit', params: { id: "page-resource-#{page.id}", value: '

    New content.

    ' } + assert_response :success + assert_equal '

    New content.

    ', page.reload.content + assert_equal '

    New content.

    ', response.body + end + + test 'a privileged user can edit a page title' do + sign_in users(:executive) + page = pages(:cluba_trail_guide) + post '/pages/edit', params: { id: "page-resource-title-#{page.id}", name: 'New Title' } + assert_response :success + assert_equal 'New Title', page.reload.name + end + + test 'an unprivileged user cannot edit a page' do + sign_in users(:member) + page = pages(:cluba_trail_guide) + original_content = page.content + post '/pages/edit', params: { id: "page-resource-#{page.id}", value: 'hacked' } + assert_response :redirect + assert_equal original_content, page.reload.content + end + + test 'a privileged user can add and delete a resources page' do + sign_in users(:executive) + assert_difference -> { clubs(:cluba).pages.count }, 1 do + post '/pages/add', params: { page: { name: 'New Page', content: '

    Hello.

    ' } } + end + assert_redirected_to '/pages/resources' + page = clubs(:cluba).pages.find_by(name: 'New Page') + assert_equal 'Resources', page.section + + assert_difference -> { clubs(:cluba).pages.count }, -1 do + post "/pages/delete/#{page.id}" + end + assert_redirected_to '/pages/resources' + end + + test 'an unprivileged user cannot add a page' do + sign_in users(:member) + assert_no_difference -> { Page.count } do + post '/pages/add', params: { page: { name: 'New Page', content: '

    Hello.

    ' } } + end + assert_response :redirect + end +end From f2c3b6a1702c3410ff3076c32efabd7296f39ab3 Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Thu, 30 Jul 2026 22:20:03 +0200 Subject: [PATCH 04/16] Add signed-token sign-in for club domains Sign-in stays on the apex domain; club domains receive the session through a 60-second signed token bound to the destination host, checked by the club-side consume endpoint. This will replace the shared cross_app_sessions table once the PHP app is retired (the legacy flow is untouched until then). - Apex /sso/authorize validates the requested host against the clubs table, routes through the sign-in form when needed, and only forwards relative return paths; /sso/logout ends the apex session - Club /users/login, /users/logout, /users/logoutComplete and other legacy URL shapes are preserved; register/profile URLs 301 to the apex - Devise hardening: remember_for drops from 20 years to 1 year, paranoid mode on, rate limiting on sign-in, password reset, and token consume - Integration tests cover the full handoff, expiry, host binding, open redirect rejection, and both-session sign-out --- app/controllers/application_controller.rb | 14 +- .../clubsite/sessions_controller.rb | 44 ++++++ app/controllers/sso_controller.rb | 47 +++++++ app/controllers/users/passwords_controller.rb | 5 + app/controllers/users/sessions_controller.rb | 5 + app/services/sso_token.rb | 21 +++ config/brakeman.ignore | 8 ++ config/initializers/0_devise.rb | 5 +- config/routes.rb | 17 ++- test/integration/clubsite_sso_test.rb | 126 ++++++++++++++++++ 10 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 app/controllers/clubsite/sessions_controller.rb create mode 100644 app/controllers/sso_controller.rb create mode 100644 app/controllers/users/passwords_controller.rb create mode 100644 app/controllers/users/sessions_controller.rb create mode 100644 app/services/sso_token.rb create mode 100644 test/integration/clubsite_sso_test.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a7eb778..34f13f4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -17,7 +17,13 @@ def cors def clear_redirect_club_if_necessary # if the user gets distracted while logging in and does something else and then goes back later to log in, we don't want to redirect them to the source club in that case - session.delete(:redirect_club_id) unless request.fullpath =~ /\/users/ + unless request.fullpath =~ /\/users/ + session.delete(:redirect_club_id) + unless request.fullpath.start_with?('/sso') + session.delete(:sso_return_host) + session.delete(:sso_return_path) + end + end end def configure_permitted_parameters @@ -34,6 +40,12 @@ def store_location end def after_sign_in_path_for(resource) + sso_return_host = session.delete(:sso_return_host) + if sso_return_host.present? + query = { return_host: sso_return_host, return_path: session.delete(:sso_return_path) }.compact.to_query + return "/sso/authorize?#{query}" + end + redirect_club_id = session[:redirect_club_id] unless redirect_club_id.nil? then club = Club.find_by_id(redirect_club_id) diff --git a/app/controllers/clubsite/sessions_controller.rb b/app/controllers/clubsite/sessions_controller.rb new file mode 100644 index 0000000..4f598c8 --- /dev/null +++ b/app/controllers/clubsite/sessions_controller.rb @@ -0,0 +1,44 @@ +module Clubsite + # Club-domain sign-in/out. There is no sign-in form on club domains: the + # visitor is sent to the apex site and comes back with a short-lived signed + # token (see SsoToken and SsoController). + class SessionsController < BaseController + rate_limit to: 10, within: 1.minute, only: :consume + + def new + query = { return_host: request.host_with_port, return_path: params[:return_path] }.compact.to_query + redirect_to "#{Settings.coreURL.chomp('/')}/sso/authorize?#{query}", + allow_other_host: true + end + + def consume + payload = SsoToken.verify(params[:token].to_s) + user = payload && payload['host'] == request.host_with_port && User.find_by(id: payload['user_id']) + if user + sign_in(user) + redirect_to safe_path + else + redirect_to '/', alert: 'Signing in failed. Please try again.' + end + end + + def destroy + sign_out(current_user) if user_signed_in? + query = { return_host: request.host_with_port }.to_query + redirect_to "#{Settings.coreURL.chomp('/')}/sso/logout?#{query}", + allow_other_host: true + end + + def logout_complete + flash[:notice] = 'You have been signed out.' + redirect_to '/' + end + + private + + def safe_path + path = params[:path].to_s + path.start_with?('/') && !path.start_with?('//') ? path : '/' + end + end +end diff --git a/app/controllers/sso_controller.rb b/app/controllers/sso_controller.rb new file mode 100644 index 0000000..578a85f --- /dev/null +++ b/app/controllers/sso_controller.rb @@ -0,0 +1,47 @@ +# Apex endpoints for signing in and out of club websites. Sign-in happens only +# on the apex domain; club domains receive the session via a short-lived +# signed token (see SsoToken and Clubsite::SessionsController). +class SsoController < ApplicationController + # Sends the visitor back to the club domain with a sign-in token, going + # through the apex sign-in form first when needed. + def authorize + club = Club.find_by(domain: params[:return_host].to_s) + if club.nil? + redirect_to root_path, alert: 'Unknown club website.' + return + end + + unless user_signed_in? + session[:sso_return_host] = club.domain + session[:sso_return_path] = safe_return_path + redirect_to new_user_session_path + return + end + + token = SsoToken.generate(current_user, club.domain) + query = { token: token, path: safe_return_path }.compact.to_query + redirect_to "#{club.domain_protocol}://#{club.domain}/sso/consume?#{query}", + allow_other_host: true + end + + # Ends the apex session as part of signing out of a club website + def logout + club = Club.find_by(domain: params[:return_host].to_s) + sign_out(current_user) if user_signed_in? + + if club + redirect_to "#{club.domain_protocol}://#{club.domain}/users/logoutComplete", + allow_other_host: true + else + redirect_to root_path + end + end + + private + + # Only relative paths may be forwarded to the club domain + def safe_return_path + path = params[:return_path].to_s + path if path.start_with?('/') && !path.start_with?('//') + end +end diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb new file mode 100644 index 0000000..e4f73a2 --- /dev/null +++ b/app/controllers/users/passwords_controller.rb @@ -0,0 +1,5 @@ +module Users + class PasswordsController < Devise::PasswordsController + rate_limit to: 5, within: 5.minutes, only: :create + end +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb new file mode 100644 index 0000000..5cbc696 --- /dev/null +++ b/app/controllers/users/sessions_controller.rb @@ -0,0 +1,5 @@ +module Users + class SessionsController < Devise::SessionsController + rate_limit to: 10, within: 1.minute, only: :create + end +end diff --git a/app/services/sso_token.rb b/app/services/sso_token.rb new file mode 100644 index 0000000..b7a55fa --- /dev/null +++ b/app/services/sso_token.rb @@ -0,0 +1,21 @@ +# Short-lived signed tokens that carry a signed-in apex session over to a club +# domain (cookies cannot span the different domains). The token is bound to +# the destination host so one leaked from logs or a referrer cannot be +# replayed on another club's domain. +class SsoToken + EXPIRY = 60.seconds + + def self.generate(user, host) + verifier.generate({ 'user_id' => user.id, 'host' => host }, + expires_in: EXPIRY, purpose: :clubsite_sso) + end + + # Returns the payload hash, or nil when invalid or expired + def self.verify(token) + verifier.verified(token, purpose: :clubsite_sso) + end + + def self.verifier + Rails.application.message_verifier(:clubsite_sso) + end +end diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 931a013..a87c0d9 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -19,6 +19,14 @@ { "fingerprint": "6f385d255ed0c43e612d0bb77992a41e96ac6c9186d2510ca2616f5ac694982d", "note": "MediaStore builds the path from the configured data folder, the club id, and an Integer-cast record id; no user-controlled path components." + }, + { + "fingerprint": "8f1cb77ac9d40ad45b479709b5709735fceded583c1e86999bc47a589252535b", + "note": "SSO sign-in redirect to a club domain; the host is resolved from the clubs table (the raw return_host param is never used) and the path is restricted to relative paths." + }, + { + "fingerprint": "7a50d993f0b0314fb42b58ae7b0e3160e1b80116b463b17351b6f9cc43ecd53f", + "note": "SSO sign-out redirect back to a club domain resolved from the clubs table." } ], "updated": "2026-07-30 00:00:00 +0000", diff --git a/config/initializers/0_devise.rb b/config/initializers/0_devise.rb index 2d97c91..b932883 100644 --- a/config/initializers/0_devise.rb +++ b/config/initializers/0_devise.rb @@ -71,7 +71,8 @@ # It will change confirmation, password recovery and other workflows # to behave the same regardless if the e-mail provided was right or wrong. # Does not affect registerable. - # config.paranoid = true + # Avoid leaking which email addresses have accounts + config.paranoid = true # By default Devise will store the user in session. You can skip storage for # :http_auth and :token_auth by adding those symbols to the array below. @@ -119,7 +120,7 @@ # ==> Configuration for :rememberable # The time the user will be remembered without asking for credentials again. - config.remember_for = 20.years + config.remember_for = 1.year # If true, extends the user's remember period when remembered via cookie. # config.extend_remember_period = false diff --git a/config/routes.rb b/config/routes.rb index bd7fe2f..de7af61 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -65,6 +65,15 @@ # Results get 'results(/index)', to: 'results#index', as: :clubsite_results + # Sign-in/out via the apex site (legacy CakePHP URL shapes preserved) + get 'users/login', to: 'sessions#new' + get 'users/logout', to: 'sessions#destroy' + get 'users/logoutComplete', to: 'sessions#logout_complete' + get 'users/localLogin', to: 'sessions#new' + get 'sso/consume', to: 'sessions#consume' + get 'users/register', to: redirect { |_params, _request| "#{Settings.coreURL.chomp('/')}/users/sign_up" }, status: 301 + get 'users/view/:id', to: redirect { |params, _request| "#{Settings.coreURL.chomp('/')}/users/#{params[:id]}" }, status: 301 + # Pages and content blocks. The jEditable editors post to the legacy # CakePHP URLs (/pages/edit, /contentBlocks/edit). post 'pages/add', to: 'pages#create' @@ -81,7 +90,13 @@ # --------------------------------------------------------------------------- # Apex (whyjustrun.ca) routes # --------------------------------------------------------------------------- - devise_for :users, :controllers => { :registrations => 'users/registrations' } + devise_for :users, :controllers => { + :registrations => 'users/registrations', + :sessions => 'users/sessions', + :passwords => 'users/passwords' + } + get 'sso/authorize', to: 'sso#authorize' + get 'sso/logout', to: 'sso#logout' root :to => "home#about_whyjustrun" get 'robots.txt', to: 'home#robots', format: false diff --git a/test/integration/clubsite_sso_test.rb b/test/integration/clubsite_sso_test.rb new file mode 100644 index 0000000..ecc8fff --- /dev/null +++ b/test/integration/clubsite_sso_test.rb @@ -0,0 +1,126 @@ +require 'test_helper' + +class ClubsiteSsoTest < ActionDispatch::IntegrationTest + setup do + @user = users(:member) + @user.update!(password: 'password123', password_confirmation: 'password123') + end + + def apex_sign_in + host! Settings.host + post '/users/sign_in', params: { user: { email: @user.email, password: 'password123' } } + end + + test "club sign-in link starts the apex authorize flow" do + host! 'cluba.test' + get '/users/login' + assert_response :redirect + assert_match %r{/sso/authorize\?return_host=cluba\.test}, response.headers['Location'] + end + + test "full handoff signs the user in on the club domain" do + apex_sign_in + + get '/sso/authorize', params: { return_host: 'cluba.test', return_path: '/events/listing' } + location = response.headers['Location'] + assert_match %r{\Ahttps://cluba\.test/sso/consume\?}, location + + query = Rack::Utils.parse_query(URI.parse(location).query) + host! 'cluba.test' + get '/sso/consume', params: { token: query['token'], path: query['path'] } + assert_redirected_to '/events/listing' + + get '/' + assert_select 'a', text: 'Sign out' + end + + test "signing in through the apex form continues to the club" do + host! 'cluba.test' + get '/users/login' + + host! Settings.host + get '/sso/authorize', params: { return_host: 'cluba.test' } + assert_redirected_to '/users/sign_in' + + post '/users/sign_in', params: { user: { email: @user.email, password: 'password123' } } + assert_match %r{/sso/authorize\?return_host=cluba\.test}, response.headers['Location'] + end + + test "expired tokens are rejected" do + apex_sign_in + get '/sso/authorize', params: { return_host: 'cluba.test' } + query = Rack::Utils.parse_query(URI.parse(response.headers['Location']).query) + + travel 2.minutes do + host! 'cluba.test' + get '/sso/consume', params: { token: query['token'] } + assert_redirected_to '/' + follow_redirect! + assert_select 'a', text: 'Sign in/Sign up' + end + end + + test "tokens are bound to the club host" do + apex_sign_in + get '/sso/authorize', params: { return_host: 'cluba.test' } + query = Rack::Utils.parse_query(URI.parse(response.headers['Location']).query) + + host! 'clubb.test' + get '/sso/consume', params: { token: query['token'] } + assert_redirected_to '/' + follow_redirect! + assert_select 'a', text: 'Sign in/Sign up' + end + + test "authorize rejects hosts that are not club domains" do + apex_sign_in + get '/sso/authorize', params: { return_host: 'evil.example.com' } + assert_redirected_to '/' + end + + test "consume only redirects to relative paths" do + apex_sign_in + get '/sso/authorize', params: { return_host: 'cluba.test' } + query = Rack::Utils.parse_query(URI.parse(response.headers['Location']).query) + + host! 'cluba.test' + get '/sso/consume', params: { token: query['token'], path: '//evil.example.com/x' } + assert_redirected_to '/' + end + + test "signing out ends both sessions" do + apex_sign_in + get '/sso/authorize', params: { return_host: 'cluba.test' } + query = Rack::Utils.parse_query(URI.parse(response.headers['Location']).query) + host! 'cluba.test' + get '/sso/consume', params: { token: query['token'] } + + get '/users/logout' + assert_match %r{/sso/logout\?return_host=cluba\.test}, response.headers['Location'] + + host! Settings.host + get '/sso/logout', params: { return_host: 'cluba.test' } + assert_redirected_to 'https://cluba.test/users/logoutComplete' + + host! 'cluba.test' + get '/users/logoutComplete' + assert_redirected_to '/' + get '/' + assert_select 'a', text: 'Sign in/Sign up' + + host! Settings.host + get '/' + assert_select 'a[href=?]', '/users/sign_in', minimum: 0 + end + + test "legacy register and profile URLs redirect to the apex" do + host! 'cluba.test' + get '/users/register' + assert_response :moved_permanently + assert_match %r{/users/sign_up\z}, response.headers['Location'] + + get "/users/view/#{@user.id}" + assert_response :moved_permanently + assert_match %r{/users/#{@user.id}\z}, response.headers['Location'] + end +end From bf915f510f0b3ae01de34be80cf84448cd68e1de Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Thu, 30 Jul 2026 22:35:59 +0200 Subject: [PATCH 05/16] Port club admin pages from Clubsite - Club settings (with Facebook page and Juicer feed URL-to-id parsing moved into the Club model), series, roles (including the JSON list the organizer picker uses), map standards, memberships, and officials - Privileges management with the level cap enforced server-side: users can only see, grant, and revoke groups at or below their own level, only within their club, and never global groups. The old app's grant endpoint had no authorization check at all; it now requires level 90 - Club branding uploads (header image, logo, custom CSS) with extension allowlists, overwrite-by-key semantics, and jpg thumbnails generated through image_processing/MiniMagick argument APIs instead of interpolated shell commands; ImageMagick and Ghostscript added to the image and CI - Thin Pundit policies per surface; all admin pages authorized server-side; all mutations POST with CSRF - 69 new integration tests; full suite 155 green, Brakeman clean --- .github/workflows/ci.yml | 2 +- Dockerfile | 2 + Gemfile | 3 + Gemfile.lock | 9 + app/controllers/clubsite/clubs_controller.rb | 43 +++ .../clubsite/map_standards_controller.rb | 46 +++ .../clubsite/memberships_controller.rb | 60 ++++ .../clubsite/officials_controller.rb | 58 +++ .../clubsite/privileges_controller.rb | 67 ++++ .../clubsite/resources_controller.rb | 36 ++ app/controllers/clubsite/roles_controller.rb | 45 +++ app/controllers/clubsite/series_controller.rb | 41 +++ app/models/club.rb | 40 +++ app/models/group.rb | 4 + app/models/membership.rb | 2 + app/models/official_classification.rb | 3 +- app/models/privilege.rb | 2 + app/models/resource.rb | 78 ++++ app/models/role.rb | 2 + app/models/series.rb | 4 + app/policies/club_policy.rb | 16 + app/policies/map_standard_policy.rb | 26 ++ app/policies/membership_policy.rb | 32 ++ app/policies/official_policy.rb | 30 ++ app/policies/privilege_policy.rb | 31 ++ app/policies/resource_policy.rb | 31 ++ app/policies/role_policy.rb | 23 ++ app/policies/series_policy.rb | 27 ++ app/services/thumbnailer.rb | 44 +++ app/views/clubsite/clubs/edit.html.erb | 106 ++++++ app/views/clubsite/clubs/index.html.erb | 22 ++ .../clubsite/map_standards/edit.html.erb | 34 ++ .../clubsite/map_standards/index.html.erb | 41 +++ app/views/clubsite/memberships/edit.html.erb | 30 ++ app/views/clubsite/memberships/index.html.erb | 49 +++ app/views/clubsite/officials/index.html.erb | 72 ++++ app/views/clubsite/privileges/index.html.erb | 57 +++ app/views/clubsite/resources/index.html.erb | 67 ++++ app/views/clubsite/roles/edit.html.erb | 25 ++ app/views/clubsite/roles/index.html.erb | 29 ++ app/views/clubsite/series/edit.html.erb | 51 +++ app/views/clubsite/series/index.html.erb | 29 ++ config/routes.rb | 65 ++++ test/fixtures/files/logo.png | Bin 0 -> 74 bytes test/fixtures/files/style.css | 3 + test/fixtures/memberships.yml | 17 + test/fixtures/official_classifications.yml | 7 + test/fixtures/officials.yml | 4 + test/integration/clubsite_admin_test.rb | 336 ++++++++++++++++++ test/integration/clubsite_privileges_test.rb | 146 ++++++++ test/integration/clubsite_resources_test.rb | 192 ++++++++++ 51 files changed, 2187 insertions(+), 2 deletions(-) create mode 100644 app/controllers/clubsite/clubs_controller.rb create mode 100644 app/controllers/clubsite/map_standards_controller.rb create mode 100644 app/controllers/clubsite/memberships_controller.rb create mode 100644 app/controllers/clubsite/officials_controller.rb create mode 100644 app/controllers/clubsite/privileges_controller.rb create mode 100644 app/controllers/clubsite/resources_controller.rb create mode 100644 app/controllers/clubsite/roles_controller.rb create mode 100644 app/controllers/clubsite/series_controller.rb create mode 100644 app/policies/club_policy.rb create mode 100644 app/policies/map_standard_policy.rb create mode 100644 app/policies/membership_policy.rb create mode 100644 app/policies/official_policy.rb create mode 100644 app/policies/privilege_policy.rb create mode 100644 app/policies/resource_policy.rb create mode 100644 app/policies/role_policy.rb create mode 100644 app/policies/series_policy.rb create mode 100644 app/services/thumbnailer.rb create mode 100644 app/views/clubsite/clubs/edit.html.erb create mode 100644 app/views/clubsite/clubs/index.html.erb create mode 100644 app/views/clubsite/map_standards/edit.html.erb create mode 100644 app/views/clubsite/map_standards/index.html.erb create mode 100644 app/views/clubsite/memberships/edit.html.erb create mode 100644 app/views/clubsite/memberships/index.html.erb create mode 100644 app/views/clubsite/officials/index.html.erb create mode 100644 app/views/clubsite/privileges/index.html.erb create mode 100644 app/views/clubsite/resources/index.html.erb create mode 100644 app/views/clubsite/roles/edit.html.erb create mode 100644 app/views/clubsite/roles/index.html.erb create mode 100644 app/views/clubsite/series/edit.html.erb create mode 100644 app/views/clubsite/series/index.html.erb create mode 100644 test/fixtures/files/logo.png create mode 100644 test/fixtures/files/style.css create mode 100644 test/fixtures/memberships.yml create mode 100644 test/fixtures/official_classifications.yml create mode 100644 test/fixtures/officials.yml create mode 100644 test/integration/clubsite_admin_test.rb create mode 100644 test/integration/clubsite_privileges_test.rb create mode 100644 test/integration/clubsite_resources_test.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d68f93..dd781c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: - uses: actions/checkout@v4 - name: Install MySQL client headers - run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev + run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev imagemagick ghostscript - uses: ruby/setup-ruby@v1 with: diff --git a/Dockerfile b/Dockerfile index 248560b..3dcc894 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,8 @@ RUN apk add --no-cache \ autoconf \ nodejs \ mariadb-dev \ + imagemagick \ + ghostscript \ bash # used in crontab generated by whenever RUN mkdir /application diff --git a/Gemfile b/Gemfile index 45a072c..138485e 100644 --- a/Gemfile +++ b/Gemfile @@ -53,6 +53,9 @@ gem "exception_notification", "~> 4.5" # Authorization gem "pundit", "~> 2.3.2" +# Image thumbnail generation (requires ImageMagick) +gem 'image_processing', '~> 1.13' + # Geo gem 'haversine', '~> 0.3.2' diff --git a/Gemfile.lock b/Gemfile.lock index a7a8f83..5a9f04a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -141,6 +141,9 @@ GEM logger ostruct ice_cube (0.17.0) + image_processing (1.14.0) + mini_magick (>= 4.9.5, < 6) + ruby-vips (>= 2.0.17, < 3) io-console (0.8.2) irb (1.17.0) pp (>= 0.6.0) @@ -167,6 +170,8 @@ GEM net-pop net-smtp marcel (1.1.0) + mini_magick (5.3.2) + logger mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.27.0) @@ -253,6 +258,9 @@ GEM responders (3.2.0) actionpack (>= 7.0) railties (>= 7.0) + ruby-vips (2.3.0) + ffi (~> 1.12) + logger sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) @@ -310,6 +318,7 @@ DEPENDENCIES geocoder (~> 1.8.3) haversine (~> 0.3.2) icalendar (~> 2.12.2) + image_processing (~> 1.13) jbuilder (~> 2.12.0) jquery-rails (~> 4.6.0) leaflet-rails (~> 1.9.3) diff --git a/app/controllers/clubsite/clubs_controller.rb b/app/controllers/clubsite/clubs_controller.rb new file mode 100644 index 0000000..c839446 --- /dev/null +++ b/app/controllers/clubsite/clubs_controller.rb @@ -0,0 +1,43 @@ +module Clubsite + # The public club list and the club settings form for webmasters. + class ClubsController < BaseController + def index + @clubs = Club.visible.order(:name) + end + + def edit + authorize current_club, :edit? + @club = current_club + set_form_collections + end + + def update + authorize current_club, :update? + @club = current_club + if @club.update(club_params) + flash[:success] = 'Updated club' + redirect_to '/pages/admin' + else + set_form_collections + render :edit + end + end + + private + + def set_form_collections + @clubs = Club.order(:name) + @club_categories = ClubCategory.all + # tz database identifiers, matching what the legacy PHP form offered and + # what the timezone column stores (e.g. America/Vancouver). + @timezones = TZInfo::Timezone.all_identifiers.sort + end + + def club_params + params.require(:club).permit(:name, :acronym, :location, :description, :url, + :facebook_page_url, :juicer_feed_url, :layout, + :timezone, :parent_id, :club_category_id, + :visible, :lat, :lng) + end + end +end diff --git a/app/controllers/clubsite/map_standards_controller.rb b/app/controllers/clubsite/map_standards_controller.rb new file mode 100644 index 0000000..6824851 --- /dev/null +++ b/app/controllers/clubsite/map_standards_controller.rb @@ -0,0 +1,46 @@ +module Clubsite + # Admin pages for map standards, shared between all clubs. + class MapStandardsController < BaseController + def index + authorize current_club, :index?, policy_class: MapStandardPolicy + @map_standards = MapStandard.all + end + + def edit + authorize current_club, :edit?, policy_class: MapStandardPolicy + @map_standard = find_or_build_map_standard + end + + def update + authorize current_club, :update?, policy_class: MapStandardPolicy + @map_standard = find_or_build_map_standard + if @map_standard.update(map_standard_params) + flash[:success] = 'The map standard has been updated.' + redirect_to '/mapStandards/' + else + render :edit + end + end + + def destroy + authorize current_club, :destroy?, policy_class: MapStandardPolicy + map_standard = MapStandard.find(params[:id]) + if map_standard.destroy + flash[:success] = 'The map standard has been deleted.' + else + flash[:danger] = 'The map standard could not be deleted.' + end + redirect_to '/mapStandards/' + end + + private + + def find_or_build_map_standard + params[:id].present? ? MapStandard.find(params[:id]) : MapStandard.new + end + + def map_standard_params + params.require(:map_standard).permit(:name, :color, :description) + end + end +end diff --git a/app/controllers/clubsite/memberships_controller.rb b/app/controllers/clubsite/memberships_controller.rb new file mode 100644 index 0000000..a0a6698 --- /dev/null +++ b/app/controllers/clubsite/memberships_controller.rb @@ -0,0 +1,60 @@ +module Clubsite + # Club membership tracking. The index doubles as the add form; edit doubles + # as add when no id is given (legacy URL shape). + class MembershipsController < BaseController + def index + authorize current_club, :index?, policy_class: MembershipPolicy + @memberships = current_club.memberships.includes(:user).order(year: :desc) + @membership = current_club.memberships.new(year: Time.zone.today.year, created: Time.zone.now) + @users = user_options + end + + def edit + @membership = find_or_build_membership + authorize @membership + @users = user_options + end + + def update + @membership = find_or_build_membership + authorize @membership + if @membership.update(membership_params) + flash[:success] = 'The membership has been updated.' + else + flash[:danger] = 'The membership could not be updated.' + end + redirect_to '/memberships/' + end + + def destroy + membership = current_club.memberships.find(params[:id]) + authorize membership + if membership.destroy + flash[:success] = 'The membership has been deleted.' + else + flash[:danger] = 'The membership could not be deleted.' + end + redirect_to '/memberships/' + end + + private + + # Scoping the find through current_club 404s ids belonging to other clubs. + def find_or_build_membership + if params[:id].present? + current_club.memberships.find(params[:id]) + else + current_club.memberships.new + end + end + + # The legacy app offered all users in the dropdown, not just club members. + def user_options + User.order(:name) + end + + def membership_params + params.require(:membership).permit(:user_id, :year, :created) + end + end +end diff --git a/app/controllers/clubsite/officials_controller.rb b/app/controllers/clubsite/officials_controller.rb new file mode 100644 index 0000000..6981d8e --- /dev/null +++ b/app/controllers/clubsite/officials_controller.rb @@ -0,0 +1,58 @@ +module Clubsite + # Officials certification tracking (user + classification + date). Officials + # are shared between all clubs; access is gated by the club privilege only. + # All forms live on the index page, so edit just returns there. + class OfficialsController < BaseController + def index + authorize current_club, :index?, policy_class: OfficialPolicy + @officials = Official.includes(:user, :official_classification) + .order(:official_classification_id) + @official_classifications = OfficialClassification.all + end + + def create + authorize current_club, :create?, policy_class: OfficialPolicy + official = Official.new(official_params) + if official.save + flash[:success] = 'The official has been added.' + else + flash[:danger] = 'The official could not be added.' + end + redirect_to '/officials/' + end + + # The legacy edit page rendered nothing; editing happens inline on index. + def edit + authorize current_club, :edit?, policy_class: OfficialPolicy + redirect_to '/officials/' + end + + def update + authorize current_club, :update?, policy_class: OfficialPolicy + official = Official.find(params[:id]) + if official.update(official_params) + flash[:success] = 'The official has been updated.' + else + flash[:danger] = 'The official could not be updated.' + end + redirect_to '/officials/' + end + + def destroy + authorize current_club, :destroy?, policy_class: OfficialPolicy + official = Official.find(params[:id]) + if official.destroy + flash[:success] = 'The official was deleted.' + else + flash[:danger] = 'The official could not be deleted.' + end + redirect_to '/officials/' + end + + private + + def official_params + params.require(:official).permit(:user_id, :official_classification_id, :date) + end + end +end diff --git a/app/controllers/clubsite/privileges_controller.rb b/app/controllers/clubsite/privileges_controller.rb new file mode 100644 index 0000000..65d28cb --- /dev/null +++ b/app/controllers/clubsite/privileges_controller.rb @@ -0,0 +1,67 @@ +module Clubsite + # Grants and revokes club privileges (group memberships). Every action + # requires the privilege edit level, and users can only see and manage + # groups at or below their own privilege level for the club -- nobody can + # grant (or revoke) a level above their own. Only groups belonging to the + # current club are managed here; global groups are never offered. + class PrivilegesController < BaseController + def index + authorize current_club, policy_class: PrivilegePolicy + @groups = manageable_groups + @privileges = Privilege.joins(:user_group, :user) + .where(groups: { id: @groups.select(:id) }) + .includes(:user, :user_group) + .order('groups.access_level DESC, users.name') + # A plain club-member select for now; the person-picker autocomplete + # arrives with the users JSON endpoint in the registration port. + @users = current_club.users.merge(User.find_all_real).order(:name) + end + + def create + authorize current_club, policy_class: PrivilegePolicy + + # Scoping to the current club 404s cross-club and global group ids + group = Group.where(club_id: current_club.id).find(privilege_params[:group_id]) + raise Pundit::NotAuthorizedError if group.access_level > acting_level + + user = User.find(privilege_params[:user_id]) + if Privilege.exists?(user: user, user_group: group) + flash[:notice] = "#{user.name} already has the #{group.name} privilege." + elsif Privilege.create(user: user, user_group: group).persisted? + flash[:success] = 'The privilege has been added.' + else + flash[:danger] = 'The privilege could not be added.' + end + redirect_to '/privileges/' + end + + def destroy + authorize current_club, policy_class: PrivilegePolicy + + privilege = Privilege.joins(:user_group) + .where(groups: { club_id: current_club.id }) + .find(params[:id]) + raise Pundit::NotAuthorizedError if privilege.user_group.access_level > acting_level + + privilege.destroy + flash[:success] = 'The privilege has been deleted.' + redirect_to '/privileges/' + end + + private + + def privilege_params + params.require(:privilege).permit(:user_id, :group_id) + end + + def acting_level + @acting_level ||= current_user.privilege_level(current_club) + end + + def manageable_groups + Group.where(club_id: current_club.id) + .up_to_level(acting_level) + .order(access_level: :desc) + end + end +end diff --git a/app/controllers/clubsite/resources_controller.rb b/app/controllers/clubsite/resources_controller.rb new file mode 100644 index 0000000..cc8493b --- /dev/null +++ b/app/controllers/clubsite/resources_controller.rb @@ -0,0 +1,36 @@ +module Clubsite + # Club branding uploads (header image, logo, custom stylesheet). Uploads are + # stored on the shared data volume and thumbnailed; see Resource. + class ResourcesController < BaseController + def index + authorize current_club, policy_class: ResourcePolicy + @resources = Resource.where(club_id: current_club.id).index_by(&:key) + @can_delete = ResourcePolicy.new(current_user, current_club).destroy? + end + + def create + authorize current_club, policy_class: ResourcePolicy + + key = params.dig(:resource, :key).to_s + not_found_404 unless Resource::RESOURCE_KEYS.key?(key) + + begin + Resource.save_for_club(current_club, key, + params.dig(:resource, :file), params.dig(:resource, :caption)) + flash[:success] = 'The resource has been uploaded.' + rescue Resource::InvalidUpload => error + flash[:danger] = error.message + end + redirect_to '/resources/index' + end + + def destroy + authorize current_club, policy_class: ResourcePolicy + + resource = Resource.where(club_id: current_club.id).find(params[:id]) + resource.destroy + flash[:success] = 'The resource has been deleted.' + redirect_to '/resources/index' + end + end +end diff --git a/app/controllers/clubsite/roles_controller.rb b/app/controllers/clubsite/roles_controller.rb new file mode 100644 index 0000000..aedabb8 --- /dev/null +++ b/app/controllers/clubsite/roles_controller.rb @@ -0,0 +1,45 @@ +module Clubsite + # Organizer roles, shared between all clubs. Any signed-in user may view the + # list (the .json variant feeds the event organizer editor); editing is + # restricted to administrators. There is intentionally no delete action: + # deleting a role would leave dangling references from organizers. + class RolesController < BaseController + def index + authorize current_club, :index?, policy_class: RolePolicy + @roles = Role.all + + respond_to do |format| + format.html + format.json do + render json: @roles.map { |role| { id: role.id, name: role.name } } + end + end + end + + def edit + authorize current_club, :edit?, policy_class: RolePolicy + @role = find_or_build_role + end + + def update + authorize current_club, :update?, policy_class: RolePolicy + @role = find_or_build_role + if @role.update(role_params) + flash[:success] = 'The role has been updated.' + redirect_to '/roles/' + else + render :edit + end + end + + private + + def find_or_build_role + params[:id].present? ? Role.find(params[:id]) : Role.new + end + + def role_params + params.require(:role).permit(:name, :description) + end + end +end diff --git a/app/controllers/clubsite/series_controller.rb b/app/controllers/clubsite/series_controller.rb new file mode 100644 index 0000000..e491c06 --- /dev/null +++ b/app/controllers/clubsite/series_controller.rb @@ -0,0 +1,41 @@ +module Clubsite + # Admin pages for the club's event series. Edit doubles as add when no id is + # given (legacy URL shape). + class SeriesController < BaseController + def index + authorize current_club, :index?, policy_class: SeriesPolicy + @series = current_club.series + end + + def edit + @series = find_or_build_series + authorize @series + end + + def update + @series = find_or_build_series + authorize @series + if @series.update(series_params) + flash[:success] = 'The series has been updated.' + redirect_to '/series/index' + else + render :edit + end + end + + private + + # Scoping the find through current_club 404s ids belonging to other clubs. + def find_or_build_series + if params[:id].present? + current_club.series.find(params[:id]) + else + current_club.series.new + end + end + + def series_params + params.require(:series).permit(:acronym, :name, :color, :information, :is_current) + end + end +end diff --git a/app/models/club.rb b/app/models/club.rb index 923d8c2..6370756 100644 --- a/app/models/club.rb +++ b/app/models/club.rb @@ -14,6 +14,13 @@ class Club < ApplicationRecord scope :visible, -> { where(visible: 1) } + validates :acronym, format: { with: /\A[a-zA-Z0-9]+\z/, message: 'may only contain letters and numbers' }, + allow_blank: true + validates :lat, :lng, numericality: true, allow_nil: true + + FACEBOOK_PAGE_URL_PREFIX = 'https://www.facebook.com/'.freeze + JUICER_FEED_URL_PREFIX = 'https://www.juicer.io/feeds/'.freeze + def children Club.where(:parent_id => id) end @@ -81,4 +88,37 @@ def national_federation def clubsite_url(path) domain_protocol + "://" + domain + path end + + # Virtual attributes for the club settings form: the admin pastes a page/feed + # URL (or a raw id) and only the id is stored. + + def facebook_page_url + facebook_page_id.present? ? FACEBOOK_PAGE_URL_PREFIX + facebook_page_id : nil + end + + def facebook_page_url=(url) + self.facebook_page_id = self.class.trailing_url_segment(url) + end + + def juicer_feed_url + juicer_feed_id.present? ? JUICER_FEED_URL_PREFIX + juicer_feed_id : nil + end + + def juicer_feed_url=(url) + self.juicer_feed_id = self.class.trailing_url_segment(url) + end + + # Extracts the last path segment of a URL ("https://www.facebook.com/foo/" + # becomes "foo"). Raw ids without slashes pass through unchanged. + def self.trailing_url_segment(value) + value = value.to_s.strip + return nil if value.blank? + + path = begin + URI.parse(value).path.to_s + rescue URI::InvalidURIError + value + end + path.delete_suffix('/').split('/').last.presence + end end diff --git a/app/models/group.rb b/app/models/group.rb index 72b7178..8403cb0 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,4 +1,8 @@ class Group < ApplicationRecord belongs_to :club has_many :privileges + + # Groups that a user with the given privilege level is allowed to see and + # manage (a user can never grant or revoke a level above their own). + scope :up_to_level, ->(level) { where(access_level: ..level) } end diff --git a/app/models/membership.rb b/app/models/membership.rb index 55591b5..c45df25 100644 --- a/app/models/membership.rb +++ b/app/models/membership.rb @@ -1,2 +1,4 @@ class Membership < ApplicationRecord + belongs_to :user + belongs_to :club end diff --git a/app/models/official_classification.rb b/app/models/official_classification.rb index a74f308..8eb125c 100644 --- a/app/models/official_classification.rb +++ b/app/models/official_classification.rb @@ -1,3 +1,4 @@ +# Officials certification levels. Shared between all clubs. class OfficialClassification < ApplicationRecord - has_many :official + has_many :officials end diff --git a/app/models/privilege.rb b/app/models/privilege.rb index d244d9d..efdf3e8 100644 --- a/app/models/privilege.rb +++ b/app/models/privilege.rb @@ -2,4 +2,6 @@ class Privilege < ApplicationRecord # avoid namespace conflict with ActiveRecord group method belongs_to :user_group, class_name: 'Group', foreign_key: 'group_id' belongs_to :user + + validates :user_id, uniqueness: { scope: :group_id } end diff --git a/app/models/resource.rb b/app/models/resource.rb index bed8411..fb500ef 100644 --- a/app/models/resource.rb +++ b/app/models/resource.rb @@ -4,12 +4,38 @@ # shared data volume as //. with jpg # thumbnails alongside. class Resource < ApplicationRecord + # Raised when an upload is rejected (missing file, disallowed extension) + class InvalidUpload < StandardError; end + THUMBNAILABLE_EXTENSIONS = %w[jpg jpeg gif png pdf].freeze # Thumbnail widths in ImageMagick resize notation THUMBNAIL_SIZES = %w[2600 1300 1000 500 100 50].freeze + # The resources a club may upload, keyed by the resource key stored in the + # database. Extensions outside the allowlist are rejected. + RESOURCE_KEYS = { + 'headerImage' => { + name: 'Header Image', + description: 'Image that will show at the top of every page. Should be at least 2600px wide.', + allowed_extensions: %w[jpg jpeg gif png] + }.freeze, + 'logo' => { + name: 'Logo', + description: 'Logo graphic', + allowed_extensions: %w[jpg jpeg gif png] + }.freeze, + 'style' => { + name: 'CSS Style', + description: 'You can override the default WhyJustRun CSS with your own style. ' \ + 'Warning: this may break with updates to WhyJustRun.', + allowed_extensions: %w[css] + }.freeze + }.freeze + belongs_to :club + before_destroy :delete_files + # Returns a map of resource keys to URLs, with additional _ # entries for thumbnailable files. Used by club layouts. def self.for_club(club) @@ -25,6 +51,30 @@ def self.for_club(club) map end + # Creates a resource for the club from an uploaded file, replacing any + # existing resource (and its files) with the same key. The original is + # stored on the data volume and jpg thumbnails are generated for + # thumbnailable file types. Raises InvalidUpload when the file is missing or + # its extension is not allowed for the key. + def self.save_for_club(club, key, upload, caption = nil) + # Callers must validate the key against RESOURCE_KEYS first; an unknown + # key here is a programming error (it becomes part of the file path). + config = RESOURCE_KEYS.fetch(key) + raise InvalidUpload, 'No file was uploaded.' if upload.blank? + + extension = File.extname(upload.original_filename).delete_prefix('.').downcase + unless config[:allowed_extensions].include?(extension) + raise InvalidUpload, "The given file extension: #{extension}, is not allowed. " \ + "Provide a file with one of: #{config[:allowed_extensions].join(', ')}" + end + + where(club_id: club.id, key: key).destroy_all + + resource = create!(club: club, key: key, caption: caption, extension: extension) + resource.store_file(upload) + resource + end + def thumbnailable? THUMBNAILABLE_EXTENSIONS.include?(extension) end @@ -41,4 +91,32 @@ def relative_path(thumbnail = nil) "#{key}.#{extension}" end end + + def absolute_path(thumbnail = nil) + File.join(Settings.dataFolder, club_id.to_s, relative_path(thumbnail)) + end + + # Writes the uploaded file to the data volume and generates thumbnails + def store_file(upload) + FileUtils.mkdir_p(File.dirname(absolute_path)) + File.open(absolute_path, 'wb') { |file| IO.copy_stream(upload.tempfile, file) } + generate_thumbnails + end + + private + + def generate_thumbnails + return unless thumbnailable? + + THUMBNAIL_SIZES.each do |size| + Thumbnailer.resize(absolute_path, absolute_path(size), size) + end + end + + # Removes the original file and any thumbnails from the data volume + def delete_files + paths = [absolute_path] + paths += THUMBNAIL_SIZES.map { |size| absolute_path(size) } if thumbnailable? + paths.each { |path| FileUtils.rm_f(path) } + end end diff --git a/app/models/role.rb b/app/models/role.rb index 5c14f94..4680548 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -1,2 +1,4 @@ +# Organizer roles (e.g. Course Planner). Shared between all clubs. class Role < ApplicationRecord + has_many :organizers end diff --git a/app/models/series.rb b/app/models/series.rb index 4b81679..f0ad283 100644 --- a/app/models/series.rb +++ b/app/models/series.rb @@ -1,3 +1,7 @@ class Series < ApplicationRecord belongs_to :club + has_many :events + + validates :acronym, format: { with: /\A[a-zA-Z0-9]+\z/, message: 'may only contain letters and numbers' }, + allow_blank: true end diff --git a/app/policies/club_policy.rb b/app/policies/club_policy.rb new file mode 100644 index 0000000..31a96af --- /dev/null +++ b/app/policies/club_policy.rb @@ -0,0 +1,16 @@ +class ClubPolicy + attr_reader :user, :club + + def initialize(user, club) + @user = user + @club = club + end + + def edit? + user.present? && user.has_privilege?(Settings.privileges.club.edit, club) + end + + def update? + edit? + end +end diff --git a/app/policies/map_standard_policy.rb b/app/policies/map_standard_policy.rb new file mode 100644 index 0000000..3e4661f --- /dev/null +++ b/app/policies/map_standard_policy.rb @@ -0,0 +1,26 @@ +# Map standards are shared between clubs, so the record is always the current +# club. +class MapStandardPolicy + attr_reader :user, :club + + def initialize(user, club) + @user = user + @club = club + end + + def index? + edit? + end + + def edit? + user.present? && user.has_privilege?(Settings.privileges.mapStandard.edit, club) + end + + def update? + edit? + end + + def destroy? + user.present? && user.has_privilege?(Settings.privileges.mapStandard.delete, club) + end +end diff --git a/app/policies/membership_policy.rb b/app/policies/membership_policy.rb new file mode 100644 index 0000000..0d66163 --- /dev/null +++ b/app/policies/membership_policy.rb @@ -0,0 +1,32 @@ +class MembershipPolicy + attr_reader :user, :record + + # record is a Membership, or a Club for collection-level checks like index. + def initialize(user, record) + @user = user + @record = record + end + + # The legacy app let any signed-in user view the members list. + def index? + user.present? + end + + def edit? + user.present? && user.has_privilege?(Settings.privileges.membership.edit, club) + end + + def update? + edit? + end + + def destroy? + user.present? && user.has_privilege?(Settings.privileges.membership.delete, club) + end + + private + + def club + record.is_a?(Club) ? record : record.club + end +end diff --git a/app/policies/official_policy.rb b/app/policies/official_policy.rb new file mode 100644 index 0000000..3afa219 --- /dev/null +++ b/app/policies/official_policy.rb @@ -0,0 +1,30 @@ +# Officials are shared between clubs, so the record is always the current +# club. +class OfficialPolicy + attr_reader :user, :club + + def initialize(user, club) + @user = user + @club = club + end + + def index? + edit? + end + + def create? + edit? + end + + def edit? + user.present? && user.has_privilege?(Settings.privileges.official.edit, club) + end + + def update? + edit? + end + + def destroy? + user.present? && user.has_privilege?(Settings.privileges.official.delete, club) + end +end diff --git a/app/policies/privilege_policy.rb b/app/policies/privilege_policy.rb new file mode 100644 index 0000000..88384c1 --- /dev/null +++ b/app/policies/privilege_policy.rb @@ -0,0 +1,31 @@ +class PrivilegePolicy + attr_reader :user, :record + + # record is a Privilege, or a Club for club-level checks like index + def initialize(user, record) + @user = user + @record = record + end + + def index? + edit_privileges? + end + + def create? + edit_privileges? + end + + def destroy? + edit_privileges? + end + + private + + def club + record.is_a?(Privilege) ? record.user_group.club : record + end + + def edit_privileges? + user.present? && user.has_privilege?(Settings.privileges.privilege.edit, club) + end +end diff --git a/app/policies/resource_policy.rb b/app/policies/resource_policy.rb new file mode 100644 index 0000000..a733835 --- /dev/null +++ b/app/policies/resource_policy.rb @@ -0,0 +1,31 @@ +class ResourcePolicy + attr_reader :user, :record + + # record is a Resource, or a Club for club-level checks like index + def initialize(user, record) + @user = user + @record = record + end + + def index? + has_privilege?(Settings.privileges.resource.index) + end + + def create? + has_privilege?(Settings.privileges.resource.edit) + end + + def destroy? + has_privilege?(Settings.privileges.resource.delete) + end + + private + + def club + record.is_a?(Resource) ? record.club : record + end + + def has_privilege?(level) + user.present? && user.has_privilege?(level, club) + end +end diff --git a/app/policies/role_policy.rb b/app/policies/role_policy.rb new file mode 100644 index 0000000..32e9c47 --- /dev/null +++ b/app/policies/role_policy.rb @@ -0,0 +1,23 @@ +# Roles are shared between clubs, so the record is always the current club. +class RolePolicy + attr_reader :user, :club + + def initialize(user, club) + @user = user + @club = club + end + + # Any signed-in user may list roles: the list feeds the event organizer + # editor (roles/index.json). + def index? + user.present? + end + + def edit? + user.present? && user.has_privilege?(Settings.privileges.role.edit, club) + end + + def update? + edit? + end +end diff --git a/app/policies/series_policy.rb b/app/policies/series_policy.rb new file mode 100644 index 0000000..0f08093 --- /dev/null +++ b/app/policies/series_policy.rb @@ -0,0 +1,27 @@ +class SeriesPolicy + attr_reader :user, :record + + # record is a Series, or a Club for collection-level checks like index. + def initialize(user, record) + @user = user + @record = record + end + + def index? + edit? + end + + def edit? + user.present? && user.has_privilege?(Settings.privileges.series.edit, club) + end + + def update? + edit? + end + + private + + def club + record.is_a?(Club) ? record : record.club + end +end diff --git a/app/services/thumbnailer.rb b/app/services/thumbnailer.rb new file mode 100644 index 0000000..dc92c9b --- /dev/null +++ b/app/services/thumbnailer.rb @@ -0,0 +1,44 @@ +require 'image_processing/mini_magick' + +# Generates jpg thumbnails from uploaded files (images and PDFs) with +# ImageMagick, via the image_processing gem. +class Thumbnailer + class MissingDependencyError < StandardError; end + + # Resizes the file at source_path into a jpg at destination_path, capped at + # `size` pixels wide. Aspect ratio is preserved and images narrower than the + # cap are never enlarged (ImageMagick's `-resize >` semantics). For + # multi-page/multi-frame sources (PDFs, animated gifs) only the first + # page/frame is used. + def self.resize(source_path, destination_path, size) + ensure_available! + + width = Integer(size) + ImageProcessing::MiniMagick + .source(source_path) + .loader(page: 0) + .resize_to_limit(width, nil) + .append('-background', 'white') + .append('-alpha', 'remove') + .append('-strip') + .append('-interlace', 'Plane') + .saver(quality: 85) + .convert('jpg') + .call(destination: destination_path) + end + + # Whether the ImageMagick command line tools are installed + def self.available? + MiniMagick.cli_version.present? + rescue StandardError + false + end + + def self.ensure_available! + return if available? + + raise MissingDependencyError, + 'ImageMagick is not installed: thumbnails cannot be generated. ' \ + 'Install the imagemagick package (and ghostscript for PDF support).' + end +end diff --git a/app/views/clubsite/clubs/edit.html.erb b/app/views/clubsite/clubs/edit.html.erb new file mode 100644 index 0000000..67e9d6e --- /dev/null +++ b/app/views/clubsite/clubs/edit.html.erb @@ -0,0 +1,106 @@ +<% content_for :title, 'Edit Club Information' %> + +<%= form_with model: @club, url: '/clubs/edit', html: { class: 'form-horizontal', 'data-validate' => 'ketchup' } do |f| %> +
    + +
    + <%= f.text_field :name, class: 'form-control', 'data-validate' => 'validate(required)', required: 'required' %> +
    +
    +
    + +
    + <%= f.text_field :acronym, class: 'form-control', 'data-validate' => 'validate(required)', required: 'required' %> +
    +
    +
    + +
    + <%= f.text_field :location, class: 'form-control', 'data-validate' => 'validate(required)', required: 'required' %> +
    +
    +
    + +
    + <%= f.text_area :description, class: 'form-control' %> +
    +
    +
    + +
    + <%= f.text_field :url, class: 'form-control', 'data-validate' => 'validate(required)', required: 'required' %> +
    +
    +
    + +
    + <%= f.text_field :facebook_page_url, class: 'form-control' %> + Used on the home page for the Facebook Like Button +
    +
    +
    + +
    + <%= f.text_field :juicer_feed_url, class: 'form-control', placeholder: 'https://www.juicer.io/feeds/orienteeringcanada' %> + Used on the home page for the News feed. You can set up/customize your feed by creating an account at juicer.io. +
    +
    +
    + +
    + <%= f.select :layout, + [['Active Club Layout', 'default'], + ['Active Club Layout - Past/Upcoming Events By Series', 'series'], + ['Inactive Club Layout', 'other']], + {}, class: 'form-control' %> +
    +
    +
    + +
    + <%= f.select :timezone, options_for_select(@timezones, @club.timezone), {}, class: 'form-control' %> +
    +
    +
    + +
    + <%= f.select :parent_id, options_from_collection_for_select(@clubs, :id, :name, @club.parent_id), + { include_blank: 'Choose the parent organisation of the club' }, class: 'form-control' %> +
    +
    +
    + +
    + <%= f.collection_select :club_category_id, @club_categories, :id, :name, {}, class: 'form-control' %> +
    +
    +
    + +
    +
    + <%= f.check_box :visible %> +
    +
    +
    +
    + +
    + <%= f.hidden_field :lat, id: 'ClubLat' %> + <%= f.hidden_field :lng, id: 'ClubLng' %> +
    +
    +

    Drag the marker to the approximate location of the club

    +
    +
    +
    +
    + <%= f.submit 'Save', class: 'btn btn-primary' %> +
    +
    +<% end %> diff --git a/app/views/clubsite/clubs/index.html.erb b/app/views/clubsite/clubs/index.html.erb new file mode 100644 index 0000000..38bf236 --- /dev/null +++ b/app/views/clubsite/clubs/index.html.erb @@ -0,0 +1,22 @@ +<% content_for :title, 'Clubs' %> + + + + + + + + + + + <% @clubs.each do |club| %> + + + + + + <% end %> + +
    NameAcronymLocation
    <%= club.url.present? ? link_to(club.name, club.url) : club.name %><%= club.acronym %><%= club.location %>
    diff --git a/app/views/clubsite/map_standards/edit.html.erb b/app/views/clubsite/map_standards/edit.html.erb new file mode 100644 index 0000000..db5e12e --- /dev/null +++ b/app/views/clubsite/map_standards/edit.html.erb @@ -0,0 +1,34 @@ +<% content_for :title, 'Map standard' %> + + +<% edit_url = @map_standard.persisted? ? "/mapStandards/edit/#{@map_standard.id}" : '/mapStandards/edit' %> +<%= form_with model: @map_standard, url: edit_url, html: { class: 'form-horizontal' } do |f| %> +
    + +
    + <%= f.text_field :name, class: 'form-control' %> +
    +
    +
    + +
    +
    + <%= f.text_field :color, value: @map_standard.color.presence || 'rgba(0,0,0,1)', class: 'form-control' %> + +
    +
    +
    +
    + +
    + <%= f.text_field :description, class: 'form-control' %> +
    +
    +
    +
    + <%= f.submit 'Save', class: 'btn btn-primary' %> +
    +
    +<% end %> diff --git a/app/views/clubsite/map_standards/index.html.erb b/app/views/clubsite/map_standards/index.html.erb new file mode 100644 index 0000000..8c76b5f --- /dev/null +++ b/app/views/clubsite/map_standards/index.html.erb @@ -0,0 +1,41 @@ +<% content_for :title, 'Map Standards' %> + +
    +
    + + + + + + + + + + + <% @map_standards.each do |map_standard| %> + + + + + + + <% end %> + +
    NameDescription
    <%= map_standard.name %><%= map_standard.description %> + + + + + <%= form_with url: "/mapStandards/delete/#{map_standard.id}", html: { class: 'thin-form' } do %> + + <% end %> +
    +
    +
    diff --git a/app/views/clubsite/memberships/edit.html.erb b/app/views/clubsite/memberships/edit.html.erb new file mode 100644 index 0000000..cb24f44 --- /dev/null +++ b/app/views/clubsite/memberships/edit.html.erb @@ -0,0 +1,30 @@ +<% content_for :title, 'Membership' %> + +<% edit_url = @membership.persisted? ? "/memberships/edit/#{@membership.id}" : '/memberships/edit' %> +<%= form_with model: @membership, url: edit_url, html: { class: 'form-horizontal' } do |f| %> +
    + <%= f.label :user_id, 'User', class: 'control-label col-sm-2' %> +
    + <%= f.collection_select :user_id, @users, :id, :name, {}, class: 'form-control' %> +
    +
    +
    + <%= f.label :year, 'Membership year', class: 'control-label col-sm-2' %> +
    + <%= f.text_field :year, class: 'form-control' %> +
    +
    +
    + <%= f.label :created, 'Created', class: 'control-label col-sm-2' %> +
    + <%= f.text_field :created, value: @membership.created&.strftime('%Y-%m-%d %H:%M:%S'), class: 'form-control' %> +
    +
    +
    +
    + <%= f.submit 'Update', class: 'btn btn-primary' %> +
    +
    +<% end %> diff --git a/app/views/clubsite/memberships/index.html.erb b/app/views/clubsite/memberships/index.html.erb new file mode 100644 index 0000000..d79c128 --- /dev/null +++ b/app/views/clubsite/memberships/index.html.erb @@ -0,0 +1,49 @@ +<% content_for :title, 'Memberships' %> +
    +

    Memberships

    +
    +
    +
    + <%= form_with model: @membership, url: '/memberships/edit' do |f| %> +
    + <%= f.label :user_id, 'User' %> + <%= f.collection_select :user_id, @users, :id, :name, {}, class: 'form-control' %> +
    +
    + <%= f.label :year, 'Membership year' %> + <%= f.text_field :year, class: 'form-control' %> +
    +
    + <%= f.label :created, 'Created' %> + <%= f.text_field :created, value: @membership.created&.strftime('%Y-%m-%d %H:%M:%S'), class: 'form-control' %> +
    + <%= f.submit 'Add membership', class: 'btn btn-primary' %> + <% end %> +
    +
    + +
    +
    + <% @memberships.group_by(&:year).each do |year, memberships| %> +

    Membership year: <%= year %>

    + + + + + + <% memberships.each do |membership| %> + + + + + + <% end %> + +
    Name
    <%= membership.user&.name %><%= link_to 'Edit', "/memberships/edit/#{membership.id}", class: 'btn btn-xs btn-default' %> + <%= form_with url: "/memberships/delete/#{membership.id}" do %> + <%= submit_tag 'Remove', class: 'btn btn-xs btn-danger' %> + <% end %> +
    + <% end %> +
    +
    diff --git a/app/views/clubsite/officials/index.html.erb b/app/views/clubsite/officials/index.html.erb new file mode 100644 index 0000000..67e56d1 --- /dev/null +++ b/app/views/clubsite/officials/index.html.erb @@ -0,0 +1,72 @@ +<% content_for :title, 'Officials' %> +
    +
    + + Clubs are strongly encouraged to track Officials' certification levels. This information is useful in many ways, including tracking numbers of qualified officials, recording pre-requisites for official certification, and identifying qualified officials for major events. +
    +
    +

    Add official

    + <%= form_with scope: :official, url: '/officials/add', html: { class: 'form-inline' } do |f| %> + <%= f.hidden_field :user_id, id: 'OfficialUserId' %> +
    + +
    +
    + <%= f.collection_select :official_classification_id, @official_classifications, :id, :name, {}, class: 'form-control' %> +
    +
    + <%= f.text_field :date, class: 'form-control', placeholder: 'Date (YYYY-MM-DD)' %> +
    + + <% end %> + +

    Current officials

    + + + + + + + + + + <% @officials.each do |official| %> + + + + + + <% end %> + +
    NameClassification/Date certified
    <%= official.user.name %> + <%= form_with scope: :official, url: "/officials/edit/#{official.id}", html: { class: 'form-inline' } do |f| %> + <%= f.hidden_field :user_id, value: official.user_id %> +
    + <%= f.collection_select :official_classification_id, @official_classifications, :id, :name, + { selected: official.official_classification_id }, + class: 'form-control input-sm' %> +
    +
    + <%= f.text_field :date, value: official.date&.strftime('%Y-%m-%d'), + class: 'form-control input-sm', placeholder: 'Date (YYYY-MM-DD)' %> +
    + <%= f.submit 'Update', class: 'btn btn-default btn-sm' %> + <% end %> +
    + <%= form_with url: "/officials/delete/#{official.id}" do %> + + <% end %> +
    +
    +
    diff --git a/app/views/clubsite/privileges/index.html.erb b/app/views/clubsite/privileges/index.html.erb new file mode 100644 index 0000000..6ff0993 --- /dev/null +++ b/app/views/clubsite/privileges/index.html.erb @@ -0,0 +1,57 @@ +<% content_for :title, 'Privileges' %> + + +
    +
    +

    Types of privileges

    +
    + <% @groups.each do |group| %> +
    <%= group.name %>
    +
    <%= group.description %>
    + <% end %> +
    + Those users not listed here can only sign up for events and have no other privileges. +
    +
    +

    Add privilege

    + <%= form_with scope: :privilege, url: '/privileges/add', html: { class: 'form-inline' } do |f| %> +
    + <%= f.collection_select :user_id, @users, :id, :name, { include_blank: 'Select a user' }, + class: 'form-control', required: true %> +
    +
    + <%= f.collection_select :group_id, @groups, :id, :name, {}, class: 'form-control' %> +
    + + <% end %> + +

    Current privileges

    + + + + + + + + + + <% @privileges.each do |privilege| %> + + + + + + <% end %> + +
    NameGroup
    <%= privilege.user.name %><%= privilege.user_group.name %> + <%= button_to "/privileges/delete/#{privilege.id}", + class: 'btn btn-sm btn-danger', form_class: 'thin-form' do %> + + <% end %> +
    +
    +
    diff --git a/app/views/clubsite/resources/index.html.erb b/app/views/clubsite/resources/index.html.erb new file mode 100644 index 0000000..6b05392 --- /dev/null +++ b/app/views/clubsite/resources/index.html.erb @@ -0,0 +1,67 @@ +<% content_for :title, 'Resources' %> + + +

    Customize your club site's design by uploading these resources.

    + +
    + + + + + + + <% if @can_delete %><% end %> + + + + <% Resource::RESOURCE_KEYS.each do |key, config| %> + <% resource = @resources[key] %> + + + + + <% if @can_delete %> + + <% end %> + + <% end %> + +
    TypeCurrentUpload
    + <%= config[:name] %> +

    <%= config[:description] %>

    +
    + <% if resource %> + <% if resource.thumbnailable? %> + <%= link_to image_tag(resource.url('100'), alt: config[:name]), resource.url %> + <% else %> + <%= link_to resource.relative_path, resource.url %> + <% end %> + <% if resource.caption.present? %> +

    <%= resource.caption %>

    + <% end %> + <% else %> + Not uploaded + <% end %> +
    + <%= form_with scope: :resource, url: '/resources/add', html: { multipart: true } do |f| %> + <%= f.hidden_field :key, value: key %> +
    + <%= f.file_field :file, required: true %> +
    +
    + <%= f.text_field :caption, placeholder: 'Caption (optional)', + class: 'form-control input-sm' %> +
    + <%= f.submit 'Upload', class: 'btn btn-primary btn-sm' %> + <% end %> +
    + <% if resource %> + <%= button_to "/resources/delete/#{resource.id}", + class: 'btn btn-sm btn-danger', form_class: 'thin-form' do %> + Delete + <% end %> + <% end %> +
    +
    diff --git a/app/views/clubsite/roles/edit.html.erb b/app/views/clubsite/roles/edit.html.erb new file mode 100644 index 0000000..f7ca72d --- /dev/null +++ b/app/views/clubsite/roles/edit.html.erb @@ -0,0 +1,25 @@ +<% content_for :title, 'Role' %> + + +<% edit_url = @role.persisted? ? "/roles/edit/#{@role.id}" : '/roles/edit' %> +<%= form_with model: @role, url: edit_url, html: { class: 'form-horizontal' } do |f| %> +
    + +
    + <%= f.text_field :name, class: 'form-control' %> +
    +
    +
    + +
    + <%= f.text_field :description, class: 'form-control' %> +
    +
    +
    +
    + <%= f.submit 'Save', class: 'btn btn-primary' %> +
    +
    +<% end %> diff --git a/app/views/clubsite/roles/index.html.erb b/app/views/clubsite/roles/index.html.erb new file mode 100644 index 0000000..0074da7 --- /dev/null +++ b/app/views/clubsite/roles/index.html.erb @@ -0,0 +1,29 @@ +<% content_for :title, 'Roles' %> + + + + + + + + + + + <% @roles.each do |role| %> + + + + + + <% end %> + +
    NameDescription
    <%= role.name %><%= role.description %> + + + +
    diff --git a/app/views/clubsite/series/edit.html.erb b/app/views/clubsite/series/edit.html.erb new file mode 100644 index 0000000..5bcdb04 --- /dev/null +++ b/app/views/clubsite/series/edit.html.erb @@ -0,0 +1,51 @@ +<% content_for :title, 'Edit Series' %> + + +<% edit_url = @series.persisted? ? "/series/edit/#{@series.id}" : '/series/edit' %> +<%= form_with model: @series, url: edit_url, html: { class: 'form-horizontal' } do |f| %> +
    + +
    + <%= f.text_field :acronym, class: 'form-control' %> +
    +
    +
    + +
    + <%= f.text_field :name, class: 'form-control' %> +
    +
    +
    + +
    +
    + <%= f.text_field :color, value: @series.color.presence || 'rgba(255,0,0,1)', class: 'form-control' %> + +
    +
    +
    +
    + +
    + <%= f.text_area :information, class: 'wjr-wysiwyg' %> +
    +

    Information specific to the series is shown on event pages.

    +
    +
    +
    +
    + +
    +
    + <%= f.check_box :is_current %> +
    +
    +
    +
    +
    + <%= f.submit 'Save', class: 'btn btn-primary' %> +
    +
    +<% end %> diff --git a/app/views/clubsite/series/index.html.erb b/app/views/clubsite/series/index.html.erb new file mode 100644 index 0000000..fa7ce45 --- /dev/null +++ b/app/views/clubsite/series/index.html.erb @@ -0,0 +1,29 @@ +<% content_for :title, 'Series' %> + + + + + + + + + + + <% @series.each do |series| %> + + + + + + <% end %> + +
    AcronymName
    <%= series.acronym %><%= series.name %> + + + +
    diff --git a/config/routes.rb b/config/routes.rb index de7af61..a39567a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -50,6 +50,15 @@ get 'events/results/:id', to: 'events#show' get 'events/map/:id', to: 'events#map' get 'events/rendering/:id', to: 'events#rendering' + get 'events/planner', to: 'events#planner' + get 'events/printableEntries/:id', to: 'events#printable_entries' + get 'events/edit(/:id)', to: 'events#edit' + post 'events/edit(/:id)', to: 'events#save' + post 'events/delete/:id', to: 'events#destroy' + get 'events/uploadMaps/:id', to: 'events#upload_maps' + get 'events/editResults/:id', to: 'events#edit_results' + post 'events/editResults/:id', to: 'events#update_results' + post 'events/toggle_live_results_visibility/:id/:visible', to: 'events#toggle_live_results_visibility' # Maps get 'maps(/index)', to: 'maps#index', as: :clubsite_maps @@ -57,13 +66,31 @@ get 'maps/report', to: 'maps#report' get 'maps/download/:id', to: 'maps#download' get 'maps/rendering/:id(/:thumbnail)', to: 'maps#rendering' + get 'maps/edit(/:id)', to: 'maps#edit' + post 'maps/edit(/:id)', to: 'maps#save' + post 'maps/delete/:id', to: 'maps#destroy' + post 'maps/update/:id/:lat/:lng', to: 'maps#update_location', + constraints: { lat: /-?[\d.]+/, lng: /-?[\d.]+/ } # Courses get 'courses/view/:id', to: 'courses#show', as: :clubsite_course get 'courses/map/:id(/:thumbnail)', to: 'courses#map' + post 'courses/register/:course_id(/:user_id)', to: 'courses#register' + post 'courses/unregister/:course_id(/:user_id)', to: 'courses#unregister' + post 'courses/delete/:id', to: 'courses#destroy' + post 'courses/uploadMap/:id', to: 'courses#upload_map' # Results get 'results(/index)', to: 'results#index', as: :clubsite_results + post 'results/editRegistrantComment', to: 'results#edit_registrant_comment' + post 'results/delete/:id', to: 'results#destroy' + + # User admin and pickers + get 'users(/index)', to: 'users#index' + post 'users/add', to: 'users#create' + post 'users/merge/:target_id/:source_id', to: 'users#merge' + get 'users/showDuplicates', to: 'users#show_duplicates' + post 'users/showDuplicates', to: 'users#show_duplicates' # Sign-in/out via the apex site (legacy CakePHP URL shapes preserved) get 'users/login', to: 'sessions#new' @@ -74,6 +101,44 @@ get 'users/register', to: redirect { |_params, _request| "#{Settings.coreURL.chomp('/')}/users/sign_up" }, status: 301 get 'users/view/:id', to: redirect { |params, _request| "#{Settings.coreURL.chomp('/')}/users/#{params[:id]}" }, status: 301 + # Club admin + get 'clubs(/index)', to: 'clubs#index' + get 'clubs/edit', to: 'clubs#edit' + post 'clubs/edit', to: 'clubs#update' + + get 'series(/index)', to: 'series#index', as: :clubsite_series + get 'series/edit(/:id)', to: 'series#edit' + post 'series/edit(/:id)', to: 'series#update' + + get 'roles(/index)', to: 'roles#index', as: :clubsite_roles + get 'roles/edit(/:id)', to: 'roles#edit' + post 'roles/edit(/:id)', to: 'roles#update' + + get 'mapStandards(/index)', to: 'map_standards#index' + get 'map_standards(/index)', to: 'map_standards#index' + get 'mapStandards/edit(/:id)', to: 'map_standards#edit' + post 'mapStandards/edit(/:id)', to: 'map_standards#update' + post 'mapStandards/delete/:id', to: 'map_standards#destroy' + + get 'memberships(/index)', to: 'memberships#index' + get 'memberships/edit(/:id)', to: 'memberships#edit' + post 'memberships/edit(/:id)', to: 'memberships#update' + post 'memberships/delete/:id', to: 'memberships#destroy' + + get 'officials(/index)', to: 'officials#index' + post 'officials/add', to: 'officials#create' + get 'officials/edit/:id', to: 'officials#edit' + post 'officials/edit/:id', to: 'officials#update' + post 'officials/delete/:id', to: 'officials#destroy' + + get 'privileges(/index)', to: 'privileges#index' + post 'privileges/add', to: 'privileges#create' + post 'privileges/delete/:id', to: 'privileges#destroy' + + get 'resources(/index)', to: 'resources#index' + post 'resources/add', to: 'resources#create' + post 'resources/delete/:id', to: 'resources#destroy' + # Pages and content blocks. The jEditable editors post to the legacy # CakePHP URLs (/pages/edit, /contentBlocks/edit). post 'pages/add', to: 'pages#create' diff --git a/test/fixtures/files/logo.png b/test/fixtures/files/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d1b0d7857f6ef592719dacf6c424f302f244660e GIT binary patch literal 74 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CB!2~3o+B%m3DM3#c$B>FS$rg&s9R&<~w3rx9 X_N6*bT^BJMD9PaI>gTe~DWM4f0*(;; literal 0 HcmV?d00001 diff --git a/test/fixtures/files/style.css b/test/fixtures/files/style.css new file mode 100644 index 0000000..3bcb744 --- /dev/null +++ b/test/fixtures/files/style.css @@ -0,0 +1,3 @@ +body { + background-color: #ffffff; +} diff --git a/test/fixtures/memberships.yml b/test/fixtures/memberships.yml new file mode 100644 index 0000000..89ca002 --- /dev/null +++ b/test/fixtures/memberships.yml @@ -0,0 +1,17 @@ +cluba_member_current: + user: member + year: 2026 + created: 2026-01-05 10:00:00 + club: cluba + +cluba_member_previous: + user: member + year: 2025 + created: 2025-01-08 09:30:00 + club: cluba + +clubb_global_admin: + user: global_admin + year: 2026 + created: 2026-01-02 12:00:00 + club: clubb diff --git a/test/fixtures/official_classifications.yml b/test/fixtures/official_classifications.yml new file mode 100644 index 0000000..615139d --- /dev/null +++ b/test/fixtures/official_classifications.yml @@ -0,0 +1,7 @@ +level_one: + name: Level 1 + description: Introductory certification + +level_two: + name: Level 2 + description: '' diff --git a/test/fixtures/officials.yml b/test/fixtures/officials.yml new file mode 100644 index 0000000..29d6122 --- /dev/null +++ b/test/fixtures/officials.yml @@ -0,0 +1,4 @@ +member_level_one: + user: member + official_classification: level_one + date: 2025-06-15 00:00:00 diff --git a/test/integration/clubsite_admin_test.rb b/test/integration/clubsite_admin_test.rb new file mode 100644 index 0000000..efcfe0d --- /dev/null +++ b/test/integration/clubsite_admin_test.rb @@ -0,0 +1,336 @@ +require 'test_helper' + +# Covers the club admin CRUD pages ported from the legacy app: clubs, series, +# roles, map standards, memberships and officials. +class ClubsiteAdminTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + # Only the tables these features touch, so the test is independent of + # fixtures belonging to other features. + self.fixture_table_names = %w[club_categories clubs groups users privileges + series roles memberships officials + official_classifications] + + setup do + host! 'cluba.test' + end + + # --- Clubs --------------------------------------------------------------- + + test 'clubs index is public and lists visible clubs' do + get '/clubs' + assert_response :success + assert_select 'td', text: 'Club A Orienteering' + assert_select 'td', text: 'Club B Orienteering' + # Invisible clubs are not listed + assert_select 'td', text: 'National Orienteering Federation', count: 0 + end + + test 'club edit renders for the webmaster' do + sign_in users(:webmaster) + get '/clubs/edit' + assert_response :success + assert_select 'form[action="/clubs/edit"]' + assert_select 'input#club_name[value=?]', 'Club A Orienteering' + # Timezone select offers tz database identifiers and preselects the club's + assert_select 'select#club_timezone option[selected][value="America/Vancouver"]' + # Marker map wiring the JS bundle expects + assert_select '.draggable-marker-map[data-lat-element="#ClubLat"]' + assert_select 'input#ClubLat' + end + + test 'club edit redirects the executive' do + sign_in users(:executive) + get '/clubs/edit' + assert_redirected_to '/' + end + + test 'club update persists and transforms pasted URLs into ids' do + sign_in users(:webmaster) + post '/clubs/edit', params: { club: { + name: 'Club A Orienteering', + acronym: 'CLUBA', + location: 'Vancouver, BC', + facebook_page_url: 'https://www.facebook.com/clubaorienteering/', + juicer_feed_url: 'https://www.juicer.io/feeds/clubafeed' + } } + assert_redirected_to '/pages/admin' + club = clubs(:cluba).reload + assert_equal 'Vancouver, BC', club.location + assert_equal 'clubaorienteering', club.facebook_page_id + assert_equal 'clubafeed', club.juicer_feed_id + end + + test 'club update cannot be performed by the executive' do + sign_in users(:executive) + post '/clubs/edit', params: { club: { name: 'Hacked' } } + assert_redirected_to '/' + assert_equal 'Club A Orienteering', clubs(:cluba).reload.name + end + + # --- Series -------------------------------------------------------------- + + test 'series index renders for the webmaster' do + sign_in users(:webmaster) + get '/series' + assert_response :success + assert_select 'td', text: 'Wednesday Evening Series' + end + + test 'series index redirects the executive' do + sign_in users(:executive) + get '/series' + assert_redirected_to '/' + end + + test 'series edit renders for the webmaster' do + sign_in users(:webmaster) + get "/series/edit/#{series(:cluba_wednesday).id}" + assert_response :success + assert_select 'input#series_name[value=?]', 'Wednesday Evening Series' + # Colorpicker markup the JS bundle expects + assert_select '.input-group.color-picker input#series_color' + end + + test 'series edit without an id adds a new series for the club' do + sign_in users(:webmaster) + assert_difference -> { clubs(:cluba).series.count }, 1 do + post '/series/edit', params: { series: { + acronym: 'SAT', name: 'Saturday Series', color: 'rgba(0,0,255,1)', is_current: '1' + } } + end + assert_redirected_to '/series/index' + series = clubs(:cluba).series.find_by(name: 'Saturday Series') + assert_equal 'SAT', series.acronym + end + + test 'series update persists' do + sign_in users(:webmaster) + post "/series/edit/#{series(:cluba_wednesday).id}", + params: { series: { name: 'Wednesday Night Series' } } + assert_redirected_to '/series/index' + assert_equal 'Wednesday Night Series', series(:cluba_wednesday).reload.name + end + + test "another club's site cannot edit this club's series" do + host! 'clubb.test' + sign_in users(:global_admin) + post "/series/edit/#{series(:cluba_wednesday).id}", + params: { series: { name: 'Hacked' } } + assert_response :not_found + assert_equal 'Wednesday Evening Series', series(:cluba_wednesday).reload.name + end + + # --- Roles --------------------------------------------------------------- + + test 'roles index renders for a plain signed-in member' do + sign_in users(:member) + get '/roles' + assert_response :success + assert_select 'td', text: 'Organizer' + assert_select 'td', text: 'Course Planner' + end + + test 'roles index redirects signed-out visitors' do + get '/roles' + assert_redirected_to '/' + end + + test 'roles index as json returns id and name pairs for a member' do + sign_in users(:member) + get '/roles/index.json' + assert_response :success + expected = Role.order(:id).map { |role| { 'id' => role.id, 'name' => role.name } } + assert_equal expected, response.parsed_body.sort_by { |role| role['id'] } + end + + test 'role edit renders for the admin' do + sign_in users(:admin) + get "/roles/edit/#{roles(:organizer).id}" + assert_response :success + assert_select 'input#role_name[value=?]', 'Organizer' + end + + test 'role edit redirects the webmaster' do + sign_in users(:webmaster) + get '/roles/edit' + assert_redirected_to '/' + end + + test 'role edit without an id adds a new role' do + sign_in users(:admin) + assert_difference -> { Role.count }, 1 do + post '/roles/edit', params: { role: { name: 'Controller', description: 'Controls the course' } } + end + assert_redirected_to '/roles/' + assert Role.exists?(name: 'Controller') + end + + # --- Map standards ------------------------------------------------------- + + test 'map standards index renders for the admin' do + sign_in users(:admin) + get '/mapStandards' + assert_response :success + assert_select 'h1', 'Map Standards' + end + + test 'map standards index redirects the webmaster' do + sign_in users(:webmaster) + get '/mapStandards' + assert_redirected_to '/' + end + + test 'map standard add, update and delete' do + sign_in users(:admin) + assert_difference -> { MapStandard.count }, 1 do + post '/mapStandards/edit', params: { map_standard: { + name: 'ISOM 2017', color: 'rgba(0,0,0,1)', description: 'Orienteering maps' + } } + end + assert_redirected_to '/mapStandards/' + map_standard = MapStandard.find_by(name: 'ISOM 2017') + + post "/mapStandards/edit/#{map_standard.id}", params: { map_standard: { name: 'ISOM 2017-2' } } + assert_redirected_to '/mapStandards/' + assert_equal 'ISOM 2017-2', map_standard.reload.name + + assert_difference -> { MapStandard.count }, -1 do + post "/mapStandards/delete/#{map_standard.id}" + end + assert_redirected_to '/mapStandards/' + end + + test 'map standard delete redirects the webmaster' do + sign_in users(:webmaster) + assert_no_difference -> { MapStandard.count } do + post '/mapStandards/delete/1' + end + assert_redirected_to '/' + end + + # --- Memberships --------------------------------------------------------- + + test 'memberships index renders for a plain signed-in member' do + sign_in users(:member) + get '/memberships' + assert_response :success + assert_select 'h3', text: 'Membership year: 2026' + assert_select 'h3', text: 'Membership year: 2025' + assert_select 'td', text: 'Mary Member' + # Memberships of other clubs are not listed + assert_select 'td', text: 'Gary Global', count: 0 + end + + test 'memberships index redirects signed-out visitors' do + get '/memberships' + assert_redirected_to '/' + end + + test 'membership edit renders for the executive' do + sign_in users(:executive) + get "/memberships/edit/#{memberships(:cluba_member_previous).id}" + assert_response :success + assert_select 'input#membership_year[value=?]', '2025' + end + + test 'membership edit redirects a plain member' do + sign_in users(:member) + get '/memberships/edit' + assert_redirected_to '/' + end + + test 'membership add, update and delete by the executive' do + sign_in users(:executive) + assert_difference -> { clubs(:cluba).memberships.count }, 1 do + post '/memberships/edit', params: { membership: { + user_id: users(:organizer).id, year: 2024, created: '2024-01-01 00:00:00' + } } + end + assert_redirected_to '/memberships/' + membership = clubs(:cluba).memberships.find_by(year: 2024) + assert_equal users(:organizer).id, membership.user_id + + post "/memberships/edit/#{membership.id}", params: { membership: { year: 2023 } } + assert_redirected_to '/memberships/' + assert_equal 2023, membership.reload.year + + assert_difference -> { clubs(:cluba).memberships.count }, -1 do + post "/memberships/delete/#{membership.id}" + end + assert_redirected_to '/memberships/' + end + + test 'a plain member cannot add a membership' do + sign_in users(:member) + assert_no_difference -> { Membership.count } do + post '/memberships/edit', params: { membership: { user_id: users(:member).id, year: 2024 } } + end + assert_redirected_to '/' + end + + test "another club's membership cannot be deleted through this club's site" do + sign_in users(:executive) + assert_no_difference -> { Membership.count } do + post "/memberships/delete/#{memberships(:clubb_global_admin).id}" + end + assert_response :not_found + end + + # --- Officials ----------------------------------------------------------- + + test 'officials index renders for the webmaster' do + sign_in users(:webmaster) + get '/officials' + assert_response :success + assert_select 'td', text: 'Mary Member' + # Add form wiring the person picker JS expects + assert_select 'form[action="/officials/add"] input#OfficialUserId' + assert_select 'input.simple-person-picker[data-user-id-target="#OfficialUserId"]' + end + + test 'officials index redirects the executive' do + sign_in users(:executive) + get '/officials' + assert_redirected_to '/' + end + + test 'official add, update and delete by the webmaster' do + sign_in users(:webmaster) + assert_difference -> { Official.count }, 1 do + post '/officials/add', params: { official: { + user_id: users(:organizer).id, + official_classification_id: official_classifications(:level_two).id, + date: '2026-07-01' + } } + end + assert_redirected_to '/officials/' + official = Official.find_by(user_id: users(:organizer).id) + assert_equal official_classifications(:level_two).id, official.official_classification_id + + post "/officials/edit/#{official.id}", params: { official: { + user_id: official.user_id, + official_classification_id: official_classifications(:level_one).id, + date: '2026-07-02' + } } + assert_redirected_to '/officials/' + assert_equal official_classifications(:level_one).id, official.reload.official_classification_id + + assert_difference -> { Official.count }, -1 do + post "/officials/delete/#{official.id}" + end + assert_redirected_to '/officials/' + end + + test 'an executive cannot add an official' do + sign_in users(:executive) + assert_no_difference -> { Official.count } do + post '/officials/add', params: { official: { + user_id: users(:organizer).id, + official_classification_id: official_classifications(:level_one).id, + date: '2026-07-01' + } } + end + assert_redirected_to '/' + end +end diff --git a/test/integration/clubsite_privileges_test.rb b/test/integration/clubsite_privileges_test.rb new file mode 100644 index 0000000..e5cb7ee --- /dev/null +++ b/test/integration/clubsite_privileges_test.rb @@ -0,0 +1,146 @@ +require 'test_helper' + +class ClubsitePrivilegesTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + # Only the tables this feature touches, so the test is independent of + # fixtures belonging to other features. + self.fixture_table_names = %w[club_categories clubs groups users privileges] + + setup do + host! 'cluba.test' + end + + test 'index lists only groups at or below the acting level' do + sign_in users(:webmaster) + get '/privileges' + assert_response :success + # The webmaster (90) sees the webmaster and executive groups but not the + # administrator group (100) + assert_select 'dt', text: 'Webmaster' + assert_select 'dt', text: 'Executive' + assert_select 'dt', text: 'Administrator', count: 0 + assert_select 'select[name="privilege[group_id]"] option', text: 'Administrator', count: 0 + # Members of visible groups are listed; administrator memberships are not + assert_select 'td', text: users(:executive).name + assert_select 'td', text: users(:admin).name, count: 0 + end + + test 'index shows all club groups to an administrator, but never global groups' do + sign_in users(:admin) + get '/privileges' + assert_response :success + assert_select 'dt', text: 'Administrator' + assert_select 'dt', text: 'Global Administrator', count: 0 + assert_select 'select[name="privilege[group_id]"] option', text: 'Global Administrator', count: 0 + end + + test 'index redirects an executive (below the privilege edit level)' do + sign_in users(:executive) + get '/privileges' + assert_redirected_to '/' + end + + test 'index redirects signed out visitors' do + get '/privileges' + assert_redirected_to '/' + end + + test 'the webmaster can grant the executive group' do + sign_in users(:webmaster) + assert_difference -> { Privilege.count }, 1 do + post '/privileges/add', + params: { privilege: { user_id: users(:member).id, group_id: groups(:cluba_executive).id } } + end + assert_redirected_to '/privileges/' + assert Privilege.exists?(user_id: users(:member).id, group_id: groups(:cluba_executive).id) + end + + test 'the webmaster cannot grant the administrator group (above their level)' do + sign_in users(:webmaster) + assert_no_difference -> { Privilege.count } do + post '/privileges/add', + params: { privilege: { user_id: users(:member).id, group_id: groups(:cluba_administrator).id } } + end + assert_redirected_to '/' + end + + test 'an executive cannot grant privileges at all' do + sign_in users(:executive) + assert_no_difference -> { Privilege.count } do + post '/privileges/add', + params: { privilege: { user_id: users(:member).id, group_id: groups(:cluba_executive).id } } + end + assert_redirected_to '/' + end + + test 'granting into a global group 404s' do + sign_in users(:admin) + assert_no_difference -> { Privilege.count } do + post '/privileges/add', + params: { privilege: { user_id: users(:member).id, group_id: groups(:global_administrator).id } } + end + assert_response :not_found + end + + test "granting into another club's group 404s" do + host! 'clubb.test' + # The global admin has level 100 on clubb via the global group + sign_in users(:global_admin) + assert_no_difference -> { Privilege.count } do + post '/privileges/add', + params: { privilege: { user_id: users(:member).id, group_id: groups(:cluba_executive).id } } + end + assert_response :not_found + end + + test 'a duplicate grant is a no-op' do + sign_in users(:webmaster) + assert_no_difference -> { Privilege.count } do + post '/privileges/add', + params: { privilege: { user_id: users(:executive).id, group_id: groups(:cluba_executive).id } } + end + assert_redirected_to '/privileges/' + end + + test 'the webmaster can revoke an executive privilege' do + sign_in users(:webmaster) + assert_difference -> { Privilege.count }, -1 do + post "/privileges/delete/#{privileges(:executive_cluba).id}" + end + assert_redirected_to '/privileges/' + end + + test 'the webmaster cannot revoke an administrator privilege (above their level)' do + sign_in users(:webmaster) + assert_no_difference -> { Privilege.count } do + post "/privileges/delete/#{privileges(:admin_cluba).id}" + end + assert_redirected_to '/' + end + + test "revoking another club's privilege 404s" do + host! 'clubb.test' + sign_in users(:global_admin) + assert_no_difference -> { Privilege.count } do + post "/privileges/delete/#{privileges(:executive_cluba).id}" + end + assert_response :not_found + end + + test 'revoking a global group privilege through a club site 404s' do + sign_in users(:admin) + assert_no_difference -> { Privilege.count } do + post "/privileges/delete/#{privileges(:global_admin_global).id}" + end + assert_response :not_found + end + + test 'an unprivileged member cannot revoke privileges' do + sign_in users(:member) + assert_no_difference -> { Privilege.count } do + post "/privileges/delete/#{privileges(:executive_cluba).id}" + end + assert_redirected_to '/' + end +end diff --git a/test/integration/clubsite_resources_test.rb b/test/integration/clubsite_resources_test.rb new file mode 100644 index 0000000..5af4510 --- /dev/null +++ b/test/integration/clubsite_resources_test.rb @@ -0,0 +1,192 @@ +require 'test_helper' + +class ClubsiteResourcesTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + # Only the tables this feature touches, so the test is independent of + # fixtures belonging to other features. + self.fixture_table_names = %w[club_categories clubs groups users privileges] + + setup do + host! 'cluba.test' + end + + teardown do + # Remove any files written to the data volume during the test + [clubs(:cluba), clubs(:clubb)].each do |club| + FileUtils.rm_rf(File.join(Settings.dataFolder, club.id.to_s)) + end + end + + test 'index renders for an executive with upload forms but no delete buttons' do + sign_in users(:executive) + get '/resources' + assert_response :success + assert_select 'strong', text: 'Header Image' + assert_select 'strong', text: 'Logo' + assert_select 'strong', text: 'CSS Style' + assert_select 'form[action="/resources/add"]', count: 3 + # Deleting requires level 90 + assert_select 'form[action^="/resources/delete"]', count: 0 + end + + test 'index redirects an unprivileged member' do + sign_in users(:member) + get '/resources' + assert_redirected_to '/' + end + + test 'index redirects signed out visitors' do + get '/resources' + assert_redirected_to '/' + end + + test 'uploading an image creates the DB row, the file and its thumbnails' do + sign_in users(:executive) + assert_difference -> { Resource.count }, 1 do + post '/resources/add', params: { + resource: { key: 'logo', file: fixture_file_upload('logo.png', 'image/png'), caption: 'Our logo' } + } + end + assert_redirected_to '/resources/index' + + resource = Resource.find_by(club_id: clubs(:cluba).id, key: 'logo') + assert_equal 'png', resource.extension + assert_equal 'Our logo', resource.caption + assert File.exist?(resource.absolute_path) + Resource::THUMBNAIL_SIZES.each do |size| + assert File.exist?(resource.absolute_path(size)), "expected #{size} thumbnail" + end + # The source is only 12px wide, so the only-shrink resize must not enlarge + thumbnail = MiniMagick::Image.open(resource.absolute_path('2600')) + assert_equal 12, thumbnail.width + + # The index now shows the thumbnail preview and, for the webmaster, delete buttons + sign_in users(:webmaster) + get '/resources' + assert_response :success + assert_select 'img[src*="logo_100.jpg"]' + assert_select "form[action=?]", "/resources/delete/#{resource.id}" + end + + test 'uploading again with the same key replaces the existing resource' do + sign_in users(:executive) + post '/resources/add', params: { + resource: { key: 'logo', file: fixture_file_upload('logo.png', 'image/png') } + } + original = Resource.find_by(club_id: clubs(:cluba).id, key: 'logo') + + assert_no_difference -> { Resource.count } do + post '/resources/add', params: { + resource: { key: 'logo', file: fixture_file_upload('logo.png', 'image/png'), caption: 'Updated' } + } + end + replacement = Resource.find_by(club_id: clubs(:cluba).id, key: 'logo') + assert_not_equal original.id, replacement.id + assert_equal 'Updated', replacement.caption + assert File.exist?(replacement.absolute_path) + end + + test 'an upload with a disallowed extension is rejected' do + sign_in users(:executive) + assert_no_difference -> { Resource.count } do + post '/resources/add', params: { + resource: { key: 'logo', file: fixture_file_upload('style.css', 'text/css') } + } + end + assert_redirected_to '/resources/index' + assert_match(/is not allowed/, flash[:danger]) + end + + test 'an upload without a file is rejected' do + sign_in users(:executive) + assert_no_difference -> { Resource.count } do + post '/resources/add', params: { resource: { key: 'logo' } } + end + assert_redirected_to '/resources/index' + assert_equal 'No file was uploaded.', flash[:danger] + end + + test 'an unknown resource key 404s' do + sign_in users(:executive) + assert_no_difference -> { Resource.count } do + post '/resources/add', params: { + resource: { key: 'evil', file: fixture_file_upload('logo.png', 'image/png') } + } + end + assert_response :not_found + end + + test 'a stylesheet upload works and generates no thumbnails' do + sign_in users(:executive) + assert_difference -> { Resource.count }, 1 do + post '/resources/add', params: { + resource: { key: 'style', file: fixture_file_upload('style.css', 'text/css') } + } + end + resource = Resource.find_by(club_id: clubs(:cluba).id, key: 'style') + assert_equal 'css', resource.extension + assert_not resource.thumbnailable? + assert File.exist?(resource.absolute_path) + Resource::THUMBNAIL_SIZES.each do |size| + assert_not File.exist?(resource.absolute_path(size)) + end + end + + test 'an unprivileged member cannot upload' do + sign_in users(:member) + assert_no_difference -> { Resource.count } do + post '/resources/add', params: { + resource: { key: 'logo', file: fixture_file_upload('logo.png', 'image/png') } + } + end + assert_redirected_to '/' + end + + test 'the webmaster can delete a resource, removing its files' do + sign_in users(:webmaster) + post '/resources/add', params: { + resource: { key: 'logo', file: fixture_file_upload('logo.png', 'image/png') } + } + resource = Resource.find_by(club_id: clubs(:cluba).id, key: 'logo') + + assert_difference -> { Resource.count }, -1 do + post "/resources/delete/#{resource.id}" + end + assert_redirected_to '/resources/index' + assert_not File.exist?(resource.absolute_path) + Resource::THUMBNAIL_SIZES.each do |size| + assert_not File.exist?(resource.absolute_path(size)) + end + end + + test 'an executive cannot delete a resource' do + sign_in users(:executive) + post '/resources/add', params: { + resource: { key: 'logo', file: fixture_file_upload('logo.png', 'image/png') } + } + resource = Resource.find_by(club_id: clubs(:cluba).id, key: 'logo') + + assert_no_difference -> { Resource.count } do + post "/resources/delete/#{resource.id}" + end + assert_redirected_to '/' + assert File.exist?(resource.absolute_path) + end + + test "another club's resource cannot be deleted through this club's site" do + sign_in users(:webmaster) + post '/resources/add', params: { + resource: { key: 'logo', file: fixture_file_upload('logo.png', 'image/png') } + } + resource = Resource.find_by(club_id: clubs(:cluba).id, key: 'logo') + + host! 'clubb.test' + sign_in users(:global_admin) + assert_no_difference -> { Resource.count } do + post "/resources/delete/#{resource.id}" + end + assert_response :not_found + assert File.exist?(resource.absolute_path) + end +end From 2b0c422f2075425c7982f59d4527b5f7e33d30c7 Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Thu, 30 Jul 2026 22:54:30 +0200 Subject: [PATCH 06/16] Port editors, uploads, and registration from Clubsite Events: - Event editor with the Knockout course and organizer editors (JSON blob contracts preserved, organizers replaced wholesale per save, dates assembled in the club's timezone), event delete at level 90 - Results editor with the legacy status/score semantics (blank status becomes ok, all-zero times become NULL) and the course-belongs-to- event check enforced before any write - Planner (now requiring the planning privilege it was always meant to have), printable entries, course map upload pages, live results visibility toggle Registration and users: - Course registration/unregistration with the legacy permission rules: anyone signed in may register others; unregistering someone else requires having registered them; duplicates are silent no-ops - Name-only fake users for registering people without accounts (now CSRF-protected), users autocomplete (now requiring sign-in), user merge as a transactional tool, duplicate finder Maps: - Map editor with image upload, generated thumbnails, and the random 60x60 banner crop folded into the upload path; marker-drag location updates now require the map edit privilege (they had no check) All remaining GET mutations are POST with CSRF; media pipelines go through MiniMagick argument APIs. Redactor loads from public/redactor when mounted at deploy time. 233 tests green, Brakeman clean. --- .gitignore | 3 +- .../clubsite/wjr/edit-event-courses.js | 2 +- app/assets/javascripts/clubsite/wjr/map.js | 8 +- .../clubsite/wjr/register-others.js | 16 +- .../javascripts/clubsite/wjr/result-editor.js | 10 +- .../clubsite/wjr/wysiwyg.redactor.js | 4 +- .../clubsite/courses_controller.rb | 98 ++++- app/controllers/clubsite/events_controller.rb | 318 ++++++++++++++- app/controllers/clubsite/maps_controller.rb | 81 +++- .../clubsite/results_controller.rb | 30 +- app/controllers/clubsite/users_controller.rb | 95 +++++ app/helpers/clubsite/layout_helper.rb | 13 + app/models/event.rb | 25 +- app/models/map.rb | 2 + app/models/result_list.rb | 5 +- app/models/tools/user_merge.rb | 31 ++ app/models/user.rb | 90 +++++ app/policies/event_policy.rb | 14 + app/policies/map_policy.rb | 15 + app/policies/result_policy.rb | 19 + app/policies/user_policy.rb | 24 ++ app/services/media_store.rb | 114 ++++++ app/views/clubsite/events/edit.html.erb | 265 +++++++++++++ .../clubsite/events/edit_results.html.erb | 95 +++++ app/views/clubsite/events/planner.html.erb | 62 +++ .../events/printable_entries.html.erb | 53 +++ .../clubsite/events/upload_maps.html.erb | 31 ++ app/views/clubsite/maps/edit.html.erb | 83 ++++ app/views/clubsite/maps/show.html.erb | 2 +- .../clubsite/users/show_duplicates.html.erb | 91 +++++ app/views/layouts/clubsite/default.html.erb | 1 + app/views/layouts/clubsite/other.html.erb | 1 + .../integration/clubsite_event_editor_test.rb | 373 ++++++++++++++++++ test/integration/clubsite_map_editor_test.rb | 180 +++++++++ .../integration/clubsite_registration_test.rb | 269 +++++++++++++ test/integration/clubsite_users_admin_test.rb | 184 +++++++++ 36 files changed, 2678 insertions(+), 29 deletions(-) create mode 100644 app/controllers/clubsite/users_controller.rb create mode 100644 app/models/tools/user_merge.rb create mode 100644 app/policies/result_policy.rb create mode 100644 app/policies/user_policy.rb create mode 100644 app/views/clubsite/events/edit.html.erb create mode 100644 app/views/clubsite/events/edit_results.html.erb create mode 100644 app/views/clubsite/events/planner.html.erb create mode 100644 app/views/clubsite/events/printable_entries.html.erb create mode 100644 app/views/clubsite/events/upload_maps.html.erb create mode 100644 app/views/clubsite/maps/edit.html.erb create mode 100644 app/views/clubsite/users/show_duplicates.html.erb create mode 100644 test/integration/clubsite_event_editor_test.rb create mode 100644 test/integration/clubsite_map_editor_test.rb create mode 100644 test/integration/clubsite_registration_test.rb create mode 100644 test/integration/clubsite_users_admin_test.rb diff --git a/.gitignore b/.gitignore index fc83913..4ecabf7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,6 @@ /.env /.env.core -/public/packs -/public/packs-test /node_modules /yarn-error.log yarn-debug.log* @@ -26,3 +24,4 @@ yarn-debug.log* vendor/ public/assets/ +/public/redactor/ diff --git a/app/assets/javascripts/clubsite/wjr/edit-event-courses.js b/app/assets/javascripts/clubsite/wjr/edit-event-courses.js index f0ec762..ebbe185 100644 --- a/app/assets/javascripts/clubsite/wjr/edit-event-courses.js +++ b/app/assets/javascripts/clubsite/wjr/edit-event-courses.js @@ -32,7 +32,7 @@ WJR['edit-event-courses'] = (function ($, _, ko) { // Only need to confirm for deletion if the course hasn't been created locally if (this.id !== null) { if (confirm("Are you sure you want to delete this course? This will also delete any results associated to the course.")) { - $.ajax('/courses/delete/' + this.id); + $.ajax('/courses/delete/' + this.id, { method: 'POST' }); viewModel.courses.remove(this); } } else { diff --git a/app/assets/javascripts/clubsite/wjr/map.js b/app/assets/javascripts/clubsite/wjr/map.js index c5f0d3d..21cc5fb 100644 --- a/app/assets/javascripts/clubsite/wjr/map.js +++ b/app/assets/javascripts/clubsite/wjr/map.js @@ -66,10 +66,11 @@ WJR.map = (function ($, _) { map.DraggableMarkerMap = function () { this.initialize = function (element) { loadGoogleMaps().then(function () { - var latElement, lngElement, zoom, googleMap, options, center, marker; + var latElement, lngElement, zoom, googleMap, options, center, marker, updateUrl; latElement = $(element.getAttribute('data-lat-element')); lngElement = $(element.getAttribute('data-lng-element')); zoom = +(element.getAttribute('data-zoom')); + updateUrl = element.getAttribute('data-update-url'); center = new google.maps.LatLng(latElement.val(), lngElement.val()); options = { @@ -88,6 +89,11 @@ WJR.map = (function ($, _) { var position = marker.getPosition(); latElement.val(position.lat()); lngElement.val(position.lng()); + if (updateUrl) { + // Persist immediately; the CSRF header comes from the global + // $.ajaxSetup in wjr/wjr.js. + $.post(updateUrl + '/' + position.lat() + '/' + position.lng()); + } }); marker.setMap(googleMap); diff --git a/app/assets/javascripts/clubsite/wjr/register-others.js b/app/assets/javascripts/clubsite/wjr/register-others.js index 1af7940..15c0f39 100644 --- a/app/assets/javascripts/clubsite/wjr/register-others.js +++ b/app/assets/javascripts/clubsite/wjr/register-others.js @@ -14,8 +14,18 @@ WJR['register-others'] = (function ($, forms) { element.find('#RegisterOthersUserId').val((person !== null) ? person.id : null); }); + // Registration is a POST; build and submit a form so the browser + // follows the redirect back to the event page. function completeSubmit(courseId, userId) { - location.href = "/courses/register/" + courseId + "/" + userId; + var form = $('', { + method: 'post', + action: '/courses/register/' + courseId + '/' + userId + }), + token = $('meta[name="csrf-token"]').attr('content'); + if (token) { + form.append($('', { type: 'hidden', name: 'authenticity_token', value: token })); + } + form.appendTo('body').submit(); } element.find('#RegisterOthersSubmit').click(function () { @@ -28,7 +38,9 @@ WJR['register-others'] = (function ($, forms) { if (userName.indexOf(" ") !== -1) { if (confirm("This registration will create a new user in the system. Are you sure " + userName + " isn't already an WhyJustRun user?")) { $.post('/users/add', { userName: userName }, function (data) { - completeSubmit(courseId, $.parseJSON(data)); + // The endpoint responds with the new user id as JSON, + // which jQuery has already parsed. + completeSubmit(courseId, data); }); } else { alert("Thanks! Please re-enter the participant's name and choose the matching person from the dropdown."); diff --git a/app/assets/javascripts/clubsite/wjr/result-editor.js b/app/assets/javascripts/clubsite/wjr/result-editor.js index 074bfc3..10750b1 100644 --- a/app/assets/javascripts/clubsite/wjr/result-editor.js +++ b/app/assets/javascripts/clubsite/wjr/result-editor.js @@ -92,7 +92,7 @@ WJR['result-editor'] = (function ($, _, ko, utils, forms) { if (!confirm("Are you sure you want to delete this competitor? The entry be deleted from the server immediately.")) { return; } - $.ajax('/results/delete/' + this.id); + $.ajax('/results/delete/' + this.id, { method: 'POST' }); } var courses = viewModel.courses(), @@ -146,14 +146,14 @@ WJR['result-editor'] = (function ($, _, ko, utils, forms) { loadObjects = function () { // TODO: We should use the result list xml api. $.getJSON('/events/view/' + eventId + '.json', function (data) { - viewModel.event(new Event(data.Event.id, data.Event.name, data.Event.date)); - var courses = data.Course; + viewModel.event(new Event(data.id, data.name, data.date)); + var courses = data.courses; _.each(courses, function (course) { - var results = course.Result, + var results = course.results, importedResults = []; _.each(results, function (result) { - var user = new User(result.User.id, result.User.name); + var user = new User(result.user.id, result.user.name); importedResults.push(new Result(result.id, user, result.course_id, result.time_seconds, result.status, result.registrant_comment, result.official_comment, result.score_points)); }); diff --git a/app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js b/app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js index 7348328..f5c0832 100644 --- a/app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js +++ b/app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js @@ -11,8 +11,8 @@ WJR.wysiwyg = (function ($) { $(element).redactor({ toolbarFixed: true, toolbarFixedBox: true, - imageUpload: '/proxies/redactor/uploadImage', - fileUpload: '/proxies/redactor/uploadFile' + imageUpload: '/api/redactor/uploadImage.json', + fileUpload: '/api/redactor/uploadFile.json' }); }; diff --git a/app/controllers/clubsite/courses_controller.rb b/app/controllers/clubsite/courses_controller.rb index 4d2857c..b042203 100644 --- a/app/controllers/clubsite/courses_controller.rb +++ b/app/controllers/clubsite/courses_controller.rb @@ -1,9 +1,13 @@ module Clubsite - # Read-only course pages: detail with results, plus serving uploaded course - # maps. Courses are reached through their event's club. + # Course pages and course actions: the detail page with results, serving + # uploaded course maps, registering and unregistering participants, and the + # course admin actions used from the event editors. Courses are reached + # through their event's club. class CoursesController < BaseController include MapsController::MediaServing + before_action :require_sign_in, only: %i[register unregister] + def show @course = Course.for_club(current_club) .includes(:event, results: :user) @@ -14,5 +18,95 @@ def show def map serve_media('Course', params[:id], params[:thumbnail]) end + + # Registers a user on a course. A missing or zero user id registers the + # signed-in user; any signed-in user may register someone else. An + # already-registered user silently redirects back to the event. + def register + course = find_course(params[:course_id]) + user = target_user + + # NOTE: The registration deadline is deliberately not enforced here. + # The legacy CakePHP action never rejected late registrations + # server-side; the UI only hides the buttons once registration closes. + unless course.results.exists?(user_id: user.id) + Result.create!(course: course, user: user, registrant: current_user) + flash[:success] = 'Registration successful!' + end + redirect_to "/events/view/#{course.event_id}" + end + + # Removes a user's registration row for this course only. You can always + # unregister yourself; you can unregister someone else only if any of + # their existing registrations (on any course) was made by you. + def unregister + course = find_course(params[:course_id]) + user_id = target_user_id + unless user_id == current_user.id || Result.exists?(registrant_id: current_user.id, user_id: user_id) + raise Pundit::NotAuthorizedError + end + + Result.where(course_id: course.id, user_id: user_id).destroy_all + flash[:success] = 'Unregistration successful!' + redirect_to "/events/view/#{course.event_id}" + end + + # Deletes a course and its results, for people who may edit the event. + # Called via AJAX from the event courses editor. + def destroy + course = find_course(params[:id]) + authorize course.event, :update? + + Course.transaction do + course.results.destroy_all + course.destroy! + end + head :ok + end + + # Stores an uploaded course map, for people who may edit the event + def upload_map + course = find_course(params[:id]) + unless policy(course.event).update? + flash[:alert] = "You aren't authorized to upload a map" + return redirect_to "/events/view/#{course.event_id}" + end + + error = + if params[:file].blank? + 'No file selected!' + else + MediaStore.new(current_club.id, 'Course').store(course.id, params[:file]) + end + + if error + flash[:alert] = error + else + flash[:success] = 'Course map uploaded!' + end + redirect_to "/events/uploadMaps/#{course.event_id}" + end + + private + + def find_course(id) + Course.for_club(current_club).find(id) + end + + # A missing or zero user id in the URL means the signed-in user (legacy + # CakePHP URL shape) + def target_user_id + id = params[:user_id].to_i + id.zero? ? current_user.id : id + end + + def target_user + id = params[:user_id].to_i + id.zero? ? current_user : User.find(id) + end + + def require_sign_in + redirect_to '/users/login' unless user_signed_in? + end end end diff --git a/app/controllers/clubsite/events_controller.rb b/app/controllers/clubsite/events_controller.rb index d2c4ed3..418045e 100644 --- a/app/controllers/clubsite/events_controller.rb +++ b/app/controllers/clubsite/events_controller.rb @@ -1,7 +1,17 @@ module Clubsite - # Read-only event pages: the calendar, the event list, and individual event - # pages with registrations, results and course maps. + # Event pages: the calendar, the event list, individual event pages with + # registrations, results and course maps, plus the event editor, results + # editor and planner. class EventsController < BaseController + # Number of extra blank rows on the printable entries list + NUM_BLANK_ENTRIES = 5 + + # Planner thresholds: people who attended at least ATTENDANCE_THRESHOLD + # events since DATE_THRESHOLD ago without organizing are suggested as + # volunteers. + PLANNER_DATE_THRESHOLD = 5.months + PLANNER_ATTENDANCE_THRESHOLD = 5 + def index if params[:date].present? && params[:date].include?('-') # Day-first date, e.g. /events/index/15-06-2025 @@ -68,8 +78,312 @@ def rendering send_file path, disposition: :inline end + # Add/edit form. Add when no id is given (legacy URL shape). + def edit + @event = find_event_or_new + authorize @event, @event.new_record? ? :create? : :update? + prepare_edit_form(@event) + end + + # Handles the edit form POST for both add and edit. + def save + @event = find_event_or_new + authorize @event, @event.new_record? ? :create? : :update? + + organizers = parse_json_blob(params.dig(:event, :organizers)) + courses = parse_json_blob(params.dig(:event, :courses)) + assign_event_fields(@event) + + if persist_event(@event, organizers, courses) + flash[:success] = 'The event has been updated.' + redirect_to "/events/view/#{@event.id}" + else + flash.now[:danger] = 'The event could not be updated.' + prepare_edit_form(@event, + organizers_json: params.dig(:event, :organizers), + courses_json: params.dig(:event, :courses)) + render :edit + end + end + + def destroy + event = current_club.events.find(params[:id]) + authorize event, :destroy? + # Destroys organizers, courses and the result list through the model + # associations; course results cascade at the database level. + event.destroy + flash[:success] = 'The event was deleted.' + redirect_to '/events/' + end + + # Planning aids: maps that haven't been used recently and regular + # attendees who haven't volunteered as organizers. + def planner + authorize Event.new(club: current_club), :plan? + @date_threshold = PLANNER_DATE_THRESHOLD.ago + @attendance_threshold = PLANNER_ATTENDANCE_THRESHOLD + @maps = planner_maps + @volunteers = planner_volunteers + end + + # Printable per-course entry list for use at the registration desk + def printable_entries + @event = current_club.events.includes(courses: { results: :user }).find(params[:id]) + @sorted_results = @event.courses.index_by(&:id) + .transform_values { |course| course.results.sort_by { |result| result.user.name } } + render layout: 'clubsite/printable' + end + + # Lists the event's courses with per-course map upload forms (the upload + # itself is handled by CoursesController#upload_map). + def upload_maps + @event = current_club.events.find(params[:id]) + authorize @event, :update? + @courses = @event.courses + end + + # Knockout-based results editor; the initial data is fetched client-side + # from the event JSON. + def edit_results + @event = current_club.events.find(params[:id]) + authorize @event, :update? + end + + # Saves the results editor form. Courses arrive as a JSON blob of + # {id, is_score_o, results: [...]} hashes. + def update_results + @event = current_club.events.find(params[:id]) + authorize @event, :update? + + allowed_course_ids = @event.courses.pluck(:id) + courses = parse_json_blob(params.dig(:event, :courses)) + + # Security check: every course being edited must belong to this event + unless (courses.map { |course| course['id'].to_i } - allowed_course_ids).empty? + return redirect_to '/', alert: 'You are not authorized to edit those courses.' + end + + begin + ActiveRecord::Base.transaction do + courses.each do |course_data| + save_course_results(course_data, allowed_course_ids) + course = @event.courses.find(course_data['id']) + course.update!(is_score_o: boolean_param(course_data['is_score_o'])) + end + @event.update!(results_posted: boolean_param(params.dig(:event, :results_posted))) if courses.any? + end + rescue ActiveRecord::RecordInvalid + flash[:danger] = 'The results could not be updated.' + return redirect_to "/events/editResults/#{@event.id}" + end + + redirect_to "/events/view/#{@event.id}" + end + + # Shows or hides the event's live result list + def toggle_live_results_visibility + @event = current_club.events.find(params[:id]) + authorize @event, :update? + live_result = ResultList.find_by(event_id: @event.id, status: ResultList::LIVE_STATUS) + live_result&.update(visible: params[:visible] == 'true') + redirect_to "/events/view/#{@event.id}" + end + private + def find_event_or_new + params[:id].present? ? current_club.events.find(params[:id]) : current_club.events.new + end + + def prepare_edit_form(event, organizers_json: nil, courses_json: nil) + @maps = current_club.maps.order(:name) + @series_options = series_options(event) + @event_classifications = EventClassification.all + @organizers_json = organizers_json || organizers_to_json(event) + @courses_json = courses_json || courses_to_json(event) + end + + # Only current series are offered, unless the event is already assigned to + # a non-current series (in which case all are offered, current first). + def series_options(event) + if event.persisted? && event.series && !event.series.is_current + current_club.series.order(is_current: :desc) + else + current_club.series.where(is_current: true) + end + end + + def organizers_to_json(event) + return '[]' if event.new_record? + + event.organizers.includes(:user, :role).map do |organizer| + { + id: organizer.user_id, + name: organizer.user.name, + role: { id: organizer.role_id, name: organizer.role&.name } + } + end.to_json + end + + def courses_to_json(event) + return '[]' if event.new_record? + + event.courses.map do |course| + { + id: course.id, + name: course.name, + distance: course.distance, + climb: course.climb, + description: course.description, + isScoreO: course.is_score_o + } + end.to_json + end + + def parse_json_blob(value) + JSON.parse(value.presence || '[]') + rescue JSON::ParserError + [] + end + + def assign_event_fields(event) + event.assign_attributes(event_params) + event.club = current_club + + # Separate date and time inputs are combined in the club's time zone + # (the request runs inside Time.use_zone). + event.date = combine_date_time(params.dig(:event, :date), params.dig(:event, :time)) + event.finish_date = combine_date_time(params.dig(:event, :finish_date), params.dig(:event, :finish_time)) + event.registration_deadline = registration_deadline_param + + # The map marker defaults to the club's location; don't store it + if event.lat.to_f == current_club.lat.to_f && event.lng.to_f == current_club.lng.to_f + event.lat = nil + event.lng = nil + end + end + + def event_params + params.require(:event).permit( + :name, :event_classification_id, :series_id, :map_id, :description, + :custom_url, :registration_url, :results_url, :routegadget_url, + :facebook_url, :attackpoint_url, :number_of_participants, :is_ranked, + :lat, :lng + ) + end + + def combine_date_time(date, time) + return nil if date.blank? + + Time.zone.parse("#{date} #{time}") + rescue ArgumentError + nil + end + + # The deadline time defaults to the end of the chosen day + def registration_deadline_param + date = params.dig(:event, :deadline_date) + return nil if date.blank? + + time = params.dig(:event, :deadline_time).presence || '23:59' + combine_date_time(date, time) + end + + def persist_event(event, organizers, courses) + ActiveRecord::Base.transaction do + # Legacy behavior: organizers are deleted and recreated on every save + event.organizers.destroy_all if event.persisted? + raise ActiveRecord::Rollback unless event.save + + organizers.each do |organizer_data| + event.organizers.create!(user_id: organizer_data['id'], + role_id: organizer_data.dig('role', 'id')) + end + + # Courses are updated in place by id and added when new; removed + # courses are deleted separately by the course editor UI. + courses.each do |course_data| + course = course_data['id'].present? ? event.courses.find(course_data['id']) : event.courses.build + course.update!(name: course_data['name'], + distance: course_data['distance'], + climb: course_data['climb'], + description: course_data['description'], + is_score_o: boolean_param(course_data['isScoreO'])) + end + + true + rescue ActiveRecord::RecordInvalid + raise ActiveRecord::Rollback + end + end + + def boolean_param(value) + ActiveModel::Type::Boolean.new.cast(value) || false + end + + # Club maps ordered by when they last hosted an event, least recently + # used first. Returns [map, last_event] pairs; last_event is nil for + # never-used maps. + def planner_maps + latest_events = current_club.events.where.not(map_id: nil).order(:date).index_by(&:map_id) + current_club.maps + .sort_by { |map| latest_events[map.id]&.date || Time.zone.at(0) } + .map { |map| [map, latest_events[map.id]] } + end + + # Club members who attended enough recent events (anywhere) but haven't + # organized any. Returns [user, attended_count] pairs, most active first. + def planner_volunteers + threshold_time = @date_threshold + attendance = Result.joins(course: :event) + .where('events.date > ?', threshold_time) + .group(:user_id) + .order(Arel.sql('COUNT(results.user_id) DESC')) + .count + attendance.select! { |_user_id, count| count >= @attendance_threshold } + + organized = Organizer.joins(:event) + .where('events.date > ?', threshold_time) + .group(:user_id) + .count + + volunteer_ids = (attendance.keys - organized.keys) & current_club.users.pluck(:id) + users = User.where(id: volunteer_ids).index_by(&:id) + volunteer_ids.filter_map { |user_id| users[user_id] && [users[user_id], attendance[user_id]] } + end + + def save_course_results(course_data, allowed_course_ids) + Array(course_data['results']).each do |result_data| + result = if result_data['id'].present? + Result.where(course_id: allowed_course_ids).find_by(id: result_data['id']) + end + result ||= Result.new + + attributes = { + user_id: result_data.dig('user', 'id'), + course_id: course_data['id'], + time_seconds: time_from_parts(result_data), + status: result_data['status'].presence || 'ok', + registrant_comment: result_data['registrant_comment'].presence, + official_comment: result_data['official_comment'].presence + } + attributes[:score_points] = result_data['score_points'] if result_data['score_points'].present? + + result.update!(attributes) + end + end + + # An all-zero time means no time was recorded + def time_from_parts(result_data) + hours = result_data['hours'].to_i + minutes = result_data['minutes'].to_i + seconds = result_data['seconds'].to_i + milliseconds = result_data['milliseconds'].to_i + return nil if hours.zero? && minutes.zero? && seconds.zero? && milliseconds.zero? + + (3600 * hours) + (60 * minutes) + seconds + (0.001 * milliseconds) + end + # .embed requests reuse the html templates; only the layout differs (see # BaseController#club_layout). def default_render diff --git a/app/controllers/clubsite/maps_controller.rb b/app/controllers/clubsite/maps_controller.rb index 54cc2bc..5d5522c 100644 --- a/app/controllers/clubsite/maps_controller.rb +++ b/app/controllers/clubsite/maps_controller.rb @@ -1,6 +1,9 @@ +# MediaStore's image pipeline uses MiniMagick, which is not autoloaded +require 'mini_magick' + module Clubsite - # Read-only map pages: listing, detail, usage report, plus serving map file - # downloads and uploaded map renderings. + # Map pages: listing, detail, usage report, the map editor, plus serving map + # file downloads and uploaded map renderings. class MapsController < BaseController # Serves uploaded media files through MediaStore. Shared with # CoursesController, which serves uploaded course maps the same way. @@ -82,8 +85,82 @@ def rendering serve_media('Map', params[:id], params[:thumbnail]) end + # Edit doubles as add when no id is given (legacy URL shape) + def edit + @map = find_or_build_map + authorize @map + @map_standards = MapStandard.all + end + + def save + @map = find_or_build_map + authorize @map + + if @map.update(map_params) + error = store_uploaded_image(@map) + if error + flash[:danger] = error + else + flash[:success] = 'The map has been updated.' + end + redirect_to "/maps/view/#{@map.id}" + else + @map_standards = MapStandard.all + render :edit + end + end + + def destroy + map = current_club.maps.find(params[:id]) + authorize map + map.destroy + map_media_store.delete(map.id) + flash[:success] = 'The map was deleted.' + redirect_to '/maps/' + end + + # Posted by the draggable marker on the edit form when the marker is + # dropped. The legacy action had no privilege check; it now requires the + # same maps.edit privilege as the rest of the editor. + def update_location + map = current_club.maps.find(params[:id]) + authorize map + map.update!(lat: params[:lat], lng: params[:lng]) + head :ok + end + private + # Scoping the find through current_club 404s ids belonging to other clubs. + def find_or_build_map + if params[:id].present? + current_club.maps.find(params[:id]) + else + current_club.maps.new + end + end + + def map_params + params.require(:map).permit(:name, :map_standard_id, :scale, :file_url, :notes, :lat, :lng) + end + + def map_media_store + MediaStore.new(current_club.id, 'Map') + end + + # Stores an uploaded map image and regenerates the randomly cropped 60x60 + # banner (the legacy generateBanner step, which ran after every upload). + # Returns an error message string on rejection, nil otherwise. + def store_uploaded_image(map) + upload = params.dig(:map, :image) + return nil if upload.blank? + + store = map_media_store + error = store.store(map.id, upload) + store.create_cropped_thumbnail(map.id, '60x60') if error.nil? + error + end + def edit_maps? user_signed_in? && MapPolicy.new(current_user, Map.new(club: current_club)).edit? end diff --git a/app/controllers/clubsite/results_controller.rb b/app/controllers/clubsite/results_controller.rb index 5d75cc7..ad19515 100644 --- a/app/controllers/clubsite/results_controller.rb +++ b/app/controllers/clubsite/results_controller.rb @@ -1,5 +1,6 @@ module Clubsite - # Read-only paginated listing of results for the club's events. + # Paginated listing of results for the club's events, plus the registrant + # comment editor and result deletion used by the event pages. class ResultsController < BaseController PER_PAGE = 20 @@ -13,5 +14,32 @@ def index .limit(PER_PAGE) .offset((@page - 1) * PER_PAGE) end + + # POST target of the comment modal on the event page. Only the person who + # made the registration or the registered user may edit the comment. + def edit_registrant_comment + return redirect_to '/users/login' unless user_signed_in? + + result = find_result(params.require(:result)[:id]) + authorize result, :edit_registrant_comment? + result.update!(registrant_comment: params[:result][:registrant_comment]) + redirect_to "/events/view/#{result.course.event_id}" + end + + # Deletes a result row (called via AJAX from the result editor), for + # people who may edit the event + def destroy + result = find_result(params[:id]) + authorize result, :destroy? + result.destroy! + head :ok + end + + private + + # Results are reachable only through the current club's events + def find_result(id) + Result.joins(course: :event).where(events: { club_id: current_club.id }).find(id) + end end end diff --git a/app/controllers/clubsite/users_controller.rb b/app/controllers/clubsite/users_controller.rb new file mode 100644 index 0000000..f1979bd --- /dev/null +++ b/app/controllers/clubsite/users_controller.rb @@ -0,0 +1,95 @@ +module Clubsite + # Club-scoped user endpoints: the person-picker JSON autocomplete, creating + # name-only ("fake") users when registering others, and the duplicate + # account merge tools. + class UsersController < BaseController + before_action :require_sign_in + + # JSON autocomplete for the person pickers. `term` is matched as a + # substring; `allowFake=false` restricts the results to accounts with an + # email address. Requiring sign-in is a deliberate hardening over the + # legacy app, which served this anonymously. + def index + users = [] + if params[:term].present? + users = User.search_by_name(params[:term]).limit(8) + users = users.merge(User.find_all_real) if params[:allowFake] == 'false' + end + render json: users.map { |user| { name: user.name, identifiableName: user.name, id: user.id } } + end + + # Creates a name-only fake user so that people without an account can be + # registered. Responds with the new user's id as a bare JSON value, the + # contract expected by register-others.js and result-editor.js. + def create + user = User.create_fake(params[:userName]) + render json: user.id + end + + def merge + authorize current_club, :merge?, policy_class: UserPolicy + perform_merge(params[:target_id], params[:source_id]) + redirect_to '/users/showDuplicates' + end + + # Lists detected duplicate accounts with merge buttons; POST performs a + # manual merge of two picked accounts + def show_duplicates + authorize current_club, :show_duplicates?, policy_class: UserPolicy + # Merge is a lower privilege level than edit. Merge-level users may only + # merge people associated with their club, whereas edit-level users can + # merge anyone. + @can_merge_any_user = UserPolicy.new(current_user, current_club).edit_any? + + if request.post? + if perform_merge(params.dig(:user, '0', :user_id), params.dig(:user, '1', :user_id)) + flash[:success] = 'Users merged' + else + flash[:notice] = 'Users not merged' + end + return redirect_to '/users/showDuplicates' + end + + @users = User.order(:name) + @duplicates = User.duplicate_sets + unless @can_merge_any_user + @duplicates = @duplicates.select do |match| + related_to_club?(match[:primary]) || related_to_club?(match[:duplicate]) + end + end + end + + private + + # Merges the source account into the target account. Missing users and + # merging an account into itself are ignored (matching the legacy + # behavior). Returns whether a merge happened. + def perform_merge(target_id, source_id) + return false if target_id.blank? || target_id.to_s == source_id.to_s + + target = User.find_by(id: target_id) + source = User.find_by(id: source_id) + return false if target.nil? || source.nil? + + Tools::UserMerge.merge(target, source) + true + end + + # A duplicate entry concerns the current club when the account or its + # most recent event belongs to the club + def related_to_club?(entry) + entry[:user].club_id == current_club.id || + entry[:most_recent_event]&.dig(:club_id) == current_club.id + end + + def require_sign_in + return if user_signed_in? + + if request.format.json? + head :unauthorized + else + redirect_to '/users/login' + end + end + end +end diff --git a/app/helpers/clubsite/layout_helper.rb b/app/helpers/clubsite/layout_helper.rb index 3a1dd77..c5f0065 100644 --- a/app/helpers/clubsite/layout_helper.rb +++ b/app/helpers/clubsite/layout_helper.rb @@ -17,5 +17,18 @@ def admin_access? def officials_access? user_signed_in? && current_user.has_privilege?(Settings.privileges.official.edit, current_club) end + + # Redactor is licensed software mounted into public/redactor at deploy + # time; rich text editing degrades gracefully when it is absent. + def redactor_available? + File.exist?(Rails.public_path.join('redactor', 'redactor.js')) + end + + def redactor_script_tags + return unless redactor_available? + + javascript_include_tag('/redactor/redactor.js', skip_pipeline: true) + + stylesheet_link_tag('/redactor/redactor.css', skip_pipeline: true) + end end end diff --git a/app/models/event.rb b/app/models/event.rb index d871057..4b3405f 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -12,13 +12,26 @@ class Event < ApplicationRecord reverse_geocoded_by :lat, :lng belongs_to :club - belongs_to :map - belongs_to :series - belongs_to :event_classification - has_many :organizers - has_many :courses + belongs_to :map, optional: true + belongs_to :series, optional: true + belongs_to :event_classification, optional: true + has_many :organizers, dependent: :destroy + has_many :courses, dependent: :destroy has_one :live_result - has_one :result_list + has_one :result_list, dependent: :destroy + + # Lenient URL check matching the legacy validation: scheme optional, + # hostname with a TLD, optional port and path. + URL_FORMAT = %r{\A(?:https?://)?(?:[\w-]+\.)+[a-z]{2,}(?::\d+)?(?:/\S*)?\z}i + + validates :name, presence: true + validates :lat, :lng, numericality: { allow_nil: true } + validates :custom_url, :registration_url, :results_url, :routegadget_url, + format: { with: URL_FORMAT }, allow_blank: true + + # The legacy app nulled empty redirect URLs so blank strings never bypass + # the custom_url.present? redirect checks. + normalizes :custom_url, with: ->(url) { url.presence } scope :list_includes, -> { includes(:series, :club, :event_classification, :courses) } diff --git a/app/models/map.rb b/app/models/map.rb index 567b7d2..48f45d1 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -4,6 +4,8 @@ class Map < ApplicationRecord belongs_to :map_standard, optional: true belongs_to :club + validates :name, presence: true + def url club.clubsite_url("/maps/view/" + id.to_s) end diff --git a/app/models/result_list.rb b/app/models/result_list.rb index dcc0894..3502b8b 100644 --- a/app/models/result_list.rb +++ b/app/models/result_list.rb @@ -5,14 +5,15 @@ class ResultList < ApplicationRecord belongs_to :event belongs_to :user - validates :type, inclusion: { + # The status column stores the result list type (live or final) + validates :status, inclusion: { in: %w(live final), message: "Result list type must be live or final" } # One-way synchronizes the result list to the results/courses tables def sync_result_list - if type != FINAL_STATUS + if status != FINAL_STATUS raise 'Can only sync results using a finalized result list' end diff --git a/app/models/tools/user_merge.rb b/app/models/tools/user_merge.rb new file mode 100644 index 0000000..6f52060 --- /dev/null +++ b/app/models/tools/user_merge.rb @@ -0,0 +1,31 @@ +module Tools + # Merges a duplicate (source) user account into a primary (target) account, + # porting the legacy CakePHP Merge behavior as configured on the User model: + # associated rows are re-pointed to the target, and a fixed set of profile + # fields is kept from the target but filled from the source when the + # target's value is NULL ('target_source' strategy). The source account is + # destroyed afterwards. The target keeps its own password. + class UserMerge + # Fields merged with the 'target_source' strategy + FILLABLE_FIELDS = %w[year_of_birth club_id si_number email referred_from].freeze + + def self.merge(target, source) + User.transaction do + Membership.where(user_id: source.id).update_all(user_id: target.id) + Organizer.where(user_id: source.id).update_all(user_id: target.id) + Result.where(user_id: source.id).update_all(user_id: target.id) + Privilege.where(user_id: source.id).update_all(user_id: target.id) + Result.where(registrant_id: source.id).update_all(registrant_id: target.id) + + FILLABLE_FIELDS.each do |field| + target[field] = source[field] if target[field].nil? + end + # Saved without validations like the legacy behavior did; the source + # row still holds the same email at this point, which would trip the + # uniqueness validation. + target.save!(validate: false) + source.destroy! + end + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index b2b68b7..5b7da87 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,6 +11,7 @@ class User < ApplicationRecord has_many :privileges has_many :resources has_many :cross_app_sessions + has_many :memberships belongs_to :club validates :name, presence: true @@ -59,6 +60,71 @@ def self.find_all_real User.where('users.email IS NOT NULL') end + # Substring name search for the person-picker autocomplete + def self.search_by_name(term) + where('users.name LIKE ?', "%#{sanitize_sql_like(term.to_s)}%") + end + + # Creates a name-only account with no email or password (a "fake" user), + # used when registering people who don't have their own account. Saved + # without validations, matching the legacy CakePHP action. + def self.create_fake(name) + user = new(name: name) + user.save!(validate: false) + user + end + + # Potential duplicate account pairs (same name, case-insensitively), each + # classified into a primary account and a duplicate account. Returns an + # array of { primary: entry, duplicate: entry } hashes where an entry is + # { user:, most_recent_event:, has_password: }. + def self.duplicate_sets + pairs = joins('INNER JOIN users AS duplicate_users ' \ + 'ON users.name LIKE duplicate_users.name AND users.id < duplicate_users.id') + .order('users.name') + .pluck('users.id', 'duplicate_users.id') + pairs.map do |first_id, second_id| + duplicate_set(includes(:club).find(first_id), includes(:club).find(second_id)) + end + end + + # Classifies a pair of same-named users into primary and duplicate using + # the legacy heuristics: a password beats no password, then the most + # recent sign-in, then the most recent event attended. + def self.duplicate_set(first, second) + events = { first.id => first.most_recent_event, second.id => second.most_recent_event } + first_date = events[first.id]&.fetch(:date) + second_date = events[second.id]&.fetch(:date) + + primary, duplicate = + if first.has_password? && !second.has_password? + [first, second] + elsif !first.has_password? && second.has_password? + [second, first] + elsif first.last_sign_in_at.present? && second.last_sign_in_at.present? + first.last_sign_in_at > second.last_sign_in_at ? [first, second] : [second, first] + elsif first.last_sign_in_at.present? + [first, second] + elsif second.last_sign_in_at.present? + [second, first] + elsif first_date.present? && second_date.present? + first_date > second_date ? [first, second] : [second, first] + elsif first_date.present? + [first, second] + elsif second_date.present? + [second, first] + else + # Out of heuristics; matches the legacy fallback + [second, first] + end + + entry = lambda do |user| + { user: user, most_recent_event: events[user.id], has_password: user.has_password? } + end + { primary: entry.call(primary), duplicate: entry.call(duplicate) } + end + private_class_method :duplicate_set + def first_name names = name.split if names.length == 1 @@ -82,6 +148,25 @@ def can_message not self.email.nil? end + # Whether this is a real account (in either password scheme) rather than a + # name-only fake user + def has_password? + old_password.present? || encrypted_password.present? + end + + # The most recent event the user has a result in, as a hash with :date, + # :club_id and :club_acronym, or nil if they have no results + def most_recent_event + event = Event.includes(:club) + .joins(courses: :results) + .where(results: { user_id: id }) + .order('events.date DESC') + .first + return nil if event.nil? + + { date: event.date, club_id: event.club_id, club_acronym: event.club&.acronym } + end + # We don't require an email for fake accounts def email_required? not password.nil? @@ -119,6 +204,11 @@ def max_privilege_level end end + # True when the user holds a membership with the club for the given year + def member_of?(club, year) + Membership.exists?(user_id: id, club_id: club.id, year: year) + end + # Find the maximum privilege level the user has for a given club def privilege_level(club) privilege = Privilege.includes(:user_group) diff --git a/app/policies/event_policy.rb b/app/policies/event_policy.rb index bcff104..2b267d5 100644 --- a/app/policies/event_policy.rb +++ b/app/policies/event_policy.rb @@ -12,4 +12,18 @@ def update? end @user.has_privilege?(Settings.privileges.event.edit, @event.club) or @event.has_organizer? @user end + + def create? + @user.present? && @user.has_privilege?(Settings.privileges.event.edit, @event.club) + end + + def destroy? + @user.present? && @user.has_privilege?(Settings.privileges.event.delete, @event.club) + end + + # Access to the event planner page. The legacy app served the page publicly + # and only gated the menu link; access now requires the planning privilege. + def plan? + @user.present? && @user.has_privilege?(Settings.privileges.event.planning, @event.club) + end end diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 3f8ad11..167b084 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -12,4 +12,19 @@ def edit? end @user.has_privilege?(Settings.privileges.maps.edit, @map.club) end + + def save? + edit? + end + + def update_location? + edit? + end + + def destroy? + unless @user + return false + end + @user.has_privilege?(Settings.privileges.maps.delete, @map.club) + end end diff --git a/app/policies/result_policy.rb b/app/policies/result_policy.rb new file mode 100644 index 0000000..506d5d5 --- /dev/null +++ b/app/policies/result_policy.rb @@ -0,0 +1,19 @@ +class ResultPolicy + attr_reader :user, :result + + def initialize(user, result) + @user = user + @result = result + end + + # Only the person who made the registration or the registered user may + # change the registrant comment + def edit_registrant_comment? + user.present? && [result.registrant_id, result.user_id].include?(user.id) + end + + # Deleting a result requires being able to edit its event + def destroy? + EventPolicy.new(user, result.course.event).update? + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 0000000..4ad9f58 --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,24 @@ +# Authorizes the club-site user admin tools. The record is the club the +# check is scoped to. +class UserPolicy + attr_reader :user, :club + + def initialize(user, club) + @user = user + @club = club + end + + def merge? + user.present? && user.has_privilege?(Settings.privileges.user.merge, club) + end + + def show_duplicates? + merge? + end + + # Users at the edit level may merge accounts unrelated to the club and get + # the manual merge form + def edit_any? + user.present? && user.has_privilege?(Settings.privileges.user.edit, club) + end +end diff --git a/app/services/media_store.rb b/app/services/media_store.rb index 7d39943..320e292 100644 --- a/app/services/media_store.rb +++ b/app/services/media_store.rb @@ -74,4 +74,118 @@ def allowed_extensions def folder Pathname.new(Settings.dataFolder).join(@club_id.to_s, @config[:dir]) end + + # Stores an uploaded file as . and generates derived images, + # replacing any previous upload. Returns an error message string on + # rejection, nil on success (matching the legacy component's contract). + def store(id, upload) + return 'No file to create was provided.' if upload.blank? + + extension = File.extname(upload.original_filename).delete_prefix('.').downcase + unless allowed_extensions.include?(extension) + return "The uploaded file format is not allowed. Please use one of #{allowed_extensions.join(',')}" + end + + delete(id) + FileUtils.mkdir_p(folder) + File.open(folder.join("#{Integer(id)}.#{extension}"), 'wb') do |file| + IO.copy_stream(upload.tempfile, file) + end + build_images(id) + nil + end + + # Removes the original upload and every derived image + def delete(id) + id = Integer(id) + original = file_path(id) + FileUtils.rm_f(original) if original + Dir.glob(folder.join("#{id}_*.{jpg,png}").to_s).each { |path| FileUtils.rm_f(path) } + end + + # Builds the normalized full-size _image.jpg (PDF pages appended into + # one tall image) and a png thumbnail per configured size, generated from + # the normalized image. + IMAGE_EXTENSIONS = %w[jpg jpeg gif png pdf].freeze + + def build_images(id) + id = Integer(id) + source = file_path(id) + return if source.nil? + # Non-image uploads (event results XML) have no derived images + return unless IMAGE_EXTENSIONS.include?(File.extname(source).delete_prefix('.').downcase) + + image_path = folder.join("#{id}_image.jpg").to_s + create_image(source, image_path, nil) + all_thumbnail_sizes.each do |size| + create_image(image_path, folder.join("#{id}_#{size}.png").to_s, size) + end + end + + # Cropped thumbnail (and its HiDPI double) taken from the original, used for + # map banners. Position 'random' picks a random crop window. + def create_cropped_thumbnail(id, size, position = 'random') + id = Integer(id) + source = file_path(id) + return if source.nil? + + doubled = self.class.double_size(size) + image = MiniMagick::Image.open(source) + crop_width, crop_height = doubled.split('x').map(&:to_i) + if position == 'random' + offset_x = rand([image.width - crop_width, 0].max + 1) + offset_y = rand([image.height - crop_height, 0].max + 1) + else + offset_x = image.width / 2 + offset_y = image.height / 2 + end + + { size => folder.join("#{id}_#{size}.png").to_s, + doubled => folder.join("#{id}_#{doubled}.png").to_s }.each do |target_size, destination| + FileUtils.rm_f(destination) + MiniMagick.convert do |convert| + # The source must be read before image operators are applied + convert << source + convert.crop("#{doubled}+#{offset_x}+#{offset_y}") + convert.strip + convert.interlace('Plane') + convert.gaussian_blur('0.05') + convert.quality('85%') + convert.resize(target_size) + convert << '+repage' + convert << destination + end + end + end + + private + + # Port of the legacy conversion pipeline: white background for transparency, + # progressive jpg, PDFs rendered at high density with all pages appended. + # All arguments go through MiniMagick's argv API; nothing touches a shell. + def create_image(source, destination, size) + FileUtils.rm_f(destination) + pdf = File.extname(source).casecmp?('.pdf') + MiniMagick.convert do |convert| + # Settings that must precede reading the source + convert.background('white') + convert.quality('85%') + if pdf + convert.density(288) + convert.colorspace('RGB') + end + convert << source + # Image operators apply once the source has been read + convert.strip + convert.interlace('Plane') + convert.gaussian_blur('0.05') + convert.alpha('remove') + if pdf + convert << '-append' + else + convert.resize("#{size}>") if size + end + convert << destination + end + end end diff --git a/app/views/clubsite/events/edit.html.erb b/app/views/clubsite/events/edit.html.erb new file mode 100644 index 0000000..4d2ebef --- /dev/null +++ b/app/views/clubsite/events/edit.html.erb @@ -0,0 +1,265 @@ +<% content_for :title, @event.new_record? ? 'Add Event' : 'Edit Event' %> + + +<% + start_time = @event.date + finish_time = @event.read_attribute(:finish_date) + deadline_time = @event.registration_deadline + edit_url = @event.persisted? ? "/events/edit/#{@event.id}" : '/events/edit' +%> +<%= form_with model: @event, scope: :event, url: edit_url, method: :post, + html: { class: 'form-horizontal', 'data-validate' => 'ketchup' } do |f| %> +
    + +
    + <%= f.text_field :name, id: 'EventName', class: 'form-control', required: 'required', + 'data-validate' => 'validate(required)' %> +
    +
    + +
    + +
    +
    +
    + <%= text_field_tag 'event[date]', start_time&.strftime('%Y-%m-%d'), + id: 'EventDate', size: 10, maxlength: 10, placeholder: 'yyyy-mm-dd', + class: 'form-control date-picker', 'data-format' => 'YYYY-MM-DD', + 'data-validate' => 'validate(date, required)' %> +
    +
    + <%= text_field_tag 'event[time]', start_time&.strftime('%H:%M'), + id: 'EventTime', size: 5, maxlength: 5, placeholder: 'hh:mm', + class: 'form-control time-picker', 'data-format' => 'HH:mm', + 'data-validate' => 'validate(required, time)' %> +
    + (24 hour format) +
    +
    +
    + +
    + +
    +
    +
    + <%= text_field_tag 'event[finish_date]', finish_time&.strftime('%Y-%m-%d'), + id: 'EventFinishDate', size: 10, maxlength: 10, placeholder: 'yyyy-mm-dd', + class: 'form-control date-picker', 'data-format' => 'YYYY-MM-DD', + 'data-validate' => 'validate(date, date_after(EventDate, EventTime, EventFinishDate, EventFinishTime))' %> +
    +
    + <%= text_field_tag 'event[finish_time]', finish_time&.strftime('%H:%M'), + id: 'EventFinishTime', size: 5, maxlength: 5, placeholder: 'hh:mm', + class: 'form-control time-picker', 'data-format' => 'HH:mm', + 'data-validate' => 'validate(time, requires(EventFinishDate, finish date))' %> +
    + (optional) +
    +
    +
    + +
    + +
    +
    +
    + <%= text_field_tag 'event[deadline_date]', deadline_time&.strftime('%Y-%m-%d'), + id: 'EventDeadlineDate', size: 10, maxlength: 10, placeholder: 'yyyy-mm-dd', + class: 'form-control date-picker', 'data-format' => 'YYYY-MM-DD', + 'data-validate' => 'validate(date, date_before(EventDate, EventTime, EventDeadlineDate, EventDeadlineTime))' %> +
    +
    + <%= text_field_tag 'event[deadline_time]', deadline_time&.strftime('%H:%M'), + id: 'EventDeadlineTime', size: 5, maxlength: 5, placeholder: 'hh:mm', + class: 'form-control time-picker', 'data-format' => 'HH:mm', + 'data-validate' => 'validate(time, requires(EventDeadlineDate, deadline date))' %> +
    + (optional) +
    +
    +
    + +
    + +
    + <%= f.select :event_classification_id, + @event_classifications.map { |classification| ["#{classification.name} (#{classification.description})", classification.id] }, + { include_blank: 'Choose classification' }, + id: 'EventEventClassificationId', class: 'form-control', required: 'required', + 'data-validate' => 'validate(required)' %> +
    +
    + + <% url_fields = [ + { name: 'Redirect URL', field: :custom_url, + placeholder: 'Only use if all info is on external website', + help: 'If you want to use a third party website for the event, enter the URL for the event page here. NOTE: People accessing your event may be automatically redirected to the other website, depending on where on WhyJustRun they come from.' }, + { name: 'Registration URL', field: :registration_url, + help: 'If you want to use a third party registration system like Zone4, enter the URL to the registration page here.' }, + { name: 'Results URL', field: :results_url, + help: 'If you post results on a third party website like WinSplits, enter the URL to the results page here after the event.' }, + { name: 'RouteGadget URL', field: :routegadget_url, + help: 'If you have posted a RouteGadget for the event, enter the URL to the RouteGadget page here after the event.' }, + { name: 'Facebook URL', field: :facebook_url, + help: 'If you have a Facebook Event, add the URL here.' }, + { name: 'Attackpoint URL', field: :attackpoint_url, + help: 'If you have an Attackpoint Event, add the URL here.' } + ] %> + <% url_fields.each do |url_field| %> +
    + +
    + <%= f.url_field url_field[:field], size: 80, class: 'form-control', + placeholder: url_field[:placeholder], + 'data-validate' => 'validate(url_or_empty)' %> +
    +
    + <% end %> + +
    + +
    + <%= f.text_area :description, id: 'EventDescription', class: 'form-control wjr-wysiwyg', rows: 12 %> +
    +
    + +
    + +
    + <%= f.collection_select :series_id, @series_options, :id, :name, + { include_blank: 'Choose the event series' }, + id: 'EventSeriesId', class: 'form-control' %> +
    +
    +
    + +
    + <%= f.collection_select :map_id, @maps, :id, :name, + { include_blank: 'Choose the event map' }, + id: 'EventMapId', class: 'form-control' %> +
    +
    + +
    + <%= f.hidden_field :organizers, value: @organizers_json, id: 'EventOrganizers', + data: { bind: 'value: ko.toJSON(organizers)' } %> + +
    + +
    + +

    + + + + +
    NameRole
    +
    +
    +
    + +
    + <%= f.hidden_field :courses, value: @courses_json, id: 'EventCourses', + data: { bind: 'value: ko.toJSON(courses)' } %> + +
    + +
    + +

    + + + + + + + + + + + + +
    NameDist. (m)Climb (m)Rank byDescription
    +
    +
    +
    + +
    + +
    +
    +
    + <%= f.number_field :number_of_participants, id: 'EventNumberOfParticipants', + min: 0, max: 4294967295, class: 'form-control' %> +
    + (Optional: used for insurance purposes, overrides participant count from event results) +
    +
    +
    + +
    + +
    +
    + <%= f.check_box :is_ranked, id: 'EventIsRanked' %> +
    +
    +
    + +
    + +
    + <%= f.hidden_field :lat, value: @event.lat || current_club.lat, id: 'EventLat' %> + <%= f.hidden_field :lng, value: @event.lng || current_club.lng, id: 'EventLng' %> +
    +
    +

    Drag the marker to the meeting location

    +
    +
    + +
    +
    + <%= f.submit 'Save', class: 'btn btn-primary' %> +
    +
    +<% end %> diff --git a/app/views/clubsite/events/edit_results.html.erb b/app/views/clubsite/events/edit_results.html.erb new file mode 100644 index 0000000..b385e65 --- /dev/null +++ b/app/views/clubsite/events/edit_results.html.erb @@ -0,0 +1,95 @@ +<% content_for :title, 'Edit Results' %> + + + + +
    + +
    +
    +
    +
    + <%= form_with url: "/events/editResults/#{@event.id}", method: :post, scope: :event do |f| %> + <%= f.hidden_field :courses, id: 'EventCourses', value: '', + data: { bind: 'value: ko.toJSON(courses)' } %> +
    +
    + +
    +
    + <%= f.submit 'Save', class: 'btn btn-primary' %> + <% end %> +
    + +
    +

    Add Competitor

    +

    Choose the course you would like to add a competitor.
    Type in the participant's name and choose the matching person. If they are not already in the system, choose the "Create New User" option.

    +
    + +
    +
    + +
    +
    +
    +
    diff --git a/app/views/clubsite/events/planner.html.erb b/app/views/clubsite/events/planner.html.erb new file mode 100644 index 0000000..f3f2db1 --- /dev/null +++ b/app/views/clubsite/events/planner.html.erb @@ -0,0 +1,62 @@ +<% content_for :title, 'Event Planner' %> + + +
    +
    +

    Which Map?

    + + + + + + + + + <% @maps.each do |map, last_event| %> + + + + + <% end %> + +
    NameLast Used
    <%= map.name %> + <% if last_event %> + + <%= time_ago_in_words(last_event.date) %> ago + + <% else %> + Never used + <% end %> +
    +
    + +
    +

    Who's Next?

    +

    A list of people who have attended at least <%= @attendance_threshold %> events since <%= time_ago_in_words(@date_threshold) %> ago, but haven't volunteered as an organizer.

    + + + + + + + + + <% @volunteers.each do |user, attended| %> + + + + + <% end %> + +
    NameAttended
    + <%= user.name %> + + <%= attended %> events +
    +
    +
    diff --git a/app/views/clubsite/events/printable_entries.html.erb b/app/views/clubsite/events/printable_entries.html.erb new file mode 100644 index 0000000..d8ba68a --- /dev/null +++ b/app/views/clubsite/events/printable_entries.html.erb @@ -0,0 +1,53 @@ +<% content_for :title, "#{@event.name} Entries" %> +

    <%= @event.name %> on <%= formatted_event_date(@event) %>

    +<% if @event.courses.any? %> + <% @event.courses.each do |course| %> + <% results = @sorted_results[course.id] %> +

    Course: <%= course.name %> (<%= results.length %> participants)

    + + + + + + + + + + + + + + + <% counter = 0 %> + <% results.each do |result| %> + <% counter += 1 %> + + + + + + + + + + + + <% end %> + <% Clubsite::EventsController::NUM_BLANK_ENTRIES.times do |i| %> + + + + + + + + + + + + <% end %> +
     NameMemberSI numberCommentXStart timeFinish timeTotal time
    <%= counter %><%= result.user.name %><%= '✓' if result.user.member_of?(current_club, Date.current.year) %><%= result.user.si_number %><%= result.registrant_comment %>    
    <%= counter + i + 1 %>        
    + <% end %> +<% else %> + No courses defined +<% end %> diff --git a/app/views/clubsite/events/upload_maps.html.erb b/app/views/clubsite/events/upload_maps.html.erb new file mode 100644 index 0000000..9f65597 --- /dev/null +++ b/app/views/clubsite/events/upload_maps.html.erb @@ -0,0 +1,31 @@ +<% content_for :title, 'Upload Course Maps' %> + + +

    Upload one map for each course. The upload process may take some time, +as thumbnails also have to be generated. Uploading will overwrite any existing maps for the particular course. Allowed map formats: jpg/jpeg, gif, png, pdf.

    +<% if @courses.any? %> + + + + + <% @courses.each do |course| %> + + + + + + <% end %> +
    CourseAdd mapCurrent Map
    <%= course.name %> + <%= form_with url: "/courses/uploadMap/#{course.id}", method: :post, scope: :course, + html: { multipart: true } do |f| %> + <%= f.file_field :image, style: 'width: 240px' %> + <%= f.submit 'Upload' %> + <% end %> + + <%= media_image('Course', course.id, '100x150') %> +
    +<% else %> +

    No courses defined.

    +<% end %> diff --git a/app/views/clubsite/maps/edit.html.erb b/app/views/clubsite/maps/edit.html.erb new file mode 100644 index 0000000..01e3577 --- /dev/null +++ b/app/views/clubsite/maps/edit.html.erb @@ -0,0 +1,83 @@ +<% content_for :title, @map.persisted? ? 'Edit Map' : 'Add Map' %> + + +<% edit_url = @map.persisted? ? "/maps/edit/#{@map.id}" : '/maps/edit' %> +<%= form_with model: @map, url: edit_url, html: { class: 'form-horizontal', multipart: true } do |f| %> +
    + +
    + <%= f.text_field :name, class: 'form-control', required: 'required' %> +
    +
    +
    + +
    + <%= f.collection_select :map_standard_id, @map_standards, :id, :name, + { include_blank: 'Choose the standard' }, class: 'form-control' %> +
    +
    +
    + +
    + <%= f.text_field :scale, class: 'form-control', placeholder: '10000' %> +
    +
    +
    + +
    + <%= f.text_field :file_url, class: 'form-control' %> +
    +
    +
    + +
    + <%= f.text_area :notes, class: 'form-control wjr-wysiwyg' %> +
    +
    +
    + +
    + <%= f.file_field :image %> + <% if @map.persisted? && media_exists?('Map', @map.id) %> +
    <%= media_image('Map', @map.id, '400x600', width: '400px', class: 'fitting-image') %> + <% end %> +
    +
    + <% if @map.persisted? && media_exists?('Map', @map.id) %> +
    + +
    + <%= media_image('Map', @map.id, '60x60', width: '60px') %> +

    The banner is regenerated from a random crop of the map image on every upload.

    +
    +
    + <% end %> +
    + +
    + <%= f.hidden_field :lat, id: 'MapLat', value: @map.lat || current_club.lat %> + <%= f.hidden_field :lng, id: 'MapLng', value: @map.lng || current_club.lng %> +
    data-update-url="/maps/update/<%= @map.id %>"<% end %> + data-zoom="10" + style="height: 400px; width: 100%"> +
    + +

    Drag the marker to the location of the map

    +
    +
    +
    +
    + <%= f.submit 'Save', class: 'btn btn-primary' %> +
    +
    +<% end %> diff --git a/app/views/clubsite/maps/show.html.erb b/app/views/clubsite/maps/show.html.erb index f4c341c..6685e10 100644 --- a/app/views/clubsite/maps/show.html.erb +++ b/app/views/clubsite/maps/show.html.erb @@ -13,7 +13,7 @@ Edit
    - + <%= button_to "/maps/delete/#{@map.id}", method: :post, class: 'btn btn-danger', form: { style: 'display: inline-block', onsubmit: "return confirm('Delete this map?');" } do %><% end %>
    <% end %> diff --git a/app/views/clubsite/users/show_duplicates.html.erb b/app/views/clubsite/users/show_duplicates.html.erb new file mode 100644 index 0000000..a310cfe --- /dev/null +++ b/app/views/clubsite/users/show_duplicates.html.erb @@ -0,0 +1,91 @@ + + +
    +
    + Merge two accounts. Copy all results, organizers, etc from + Duplicate account to Primary account. Then delete + Duplicate account. If Duplicate account has user attributes that are + NULL in Primary account, then use these. The password of + Primary account will be used. +
    +
    +

    Detected duplicates

    + These accounts were determined to be duplicates. Duplicates exist for accounts + that have similar names. The algorithm for determining which of the two + accounts is the primary account is as follows: +
      +
    • Check if only one account has a password (i.e. it is a 'real' account)
    • +
    • Check which account has been logged in with most recent (not used)
    • +
    • Check which account has been registered for the most recent event
    • +
    • Check which account was created first
    • +
    +
    + + + + + + + + + + + + + + + + + + + + + <% @duplicates.each do |match| %> + + <% [match[:primary], match[:duplicate]].each do |entry| %> + + + + + <% end %> + + + <% end %> + +
    Primary accountDuplicate account
    NameClubIDMost recent event (club)NameClubIDMost recent event (club)
    + <%= entry[:user].name %> + <% if entry[:has_password] %> + Real + <% else %> + Fake + <% end %> + <%= entry[:user].club&.acronym %><%= entry[:user].id %> + <% if entry[:most_recent_event] %> + <%= entry[:most_recent_event][:date].to_date %><% if entry[:most_recent_event][:club_acronym].present? %> (<%= entry[:most_recent_event][:club_acronym] %>)<% end %> + <% end %> + + <%= button_to 'Merge', "/users/merge/#{match[:primary][:user].id}/#{match[:duplicate][:user].id}", class: 'btn btn-default' %> +
    +
    +
    + <% if @can_merge_any_user %> +
    +

    Manual merge

    + <%= form_tag '/users/showDuplicates' do %> +
    + + <%= select_tag 'user[0][user_id]', options_from_collection_for_select(@users, :id, :name), + id: 'manual-merge-primary', class: 'form-control' %> +
    +
    + + <%= select_tag 'user[1][user_id]', options_from_collection_for_select(@users, :id, :name), + id: 'manual-merge-duplicate', class: 'form-control' %> +
    + <%= submit_tag 'Merge', class: 'btn btn-default' %> + <% end %> +
    + <% end %> +
    diff --git a/app/views/layouts/clubsite/default.html.erb b/app/views/layouts/clubsite/default.html.erb index 7546b79..4466b23 100644 --- a/app/views/layouts/clubsite/default.html.erb +++ b/app/views/layouts/clubsite/default.html.erb @@ -72,6 +72,7 @@ <%= javascript_include_tag 'clubsite' %> + <%= redactor_script_tags %> <%= yield :javascript_bottom %> diff --git a/app/views/layouts/clubsite/other.html.erb b/app/views/layouts/clubsite/other.html.erb index 9327649..c178bc7 100644 --- a/app/views/layouts/clubsite/other.html.erb +++ b/app/views/layouts/clubsite/other.html.erb @@ -50,6 +50,7 @@ <%= javascript_include_tag 'clubsite' %> + <%= redactor_script_tags %> <%= yield :javascript_bottom %> diff --git a/test/integration/clubsite_event_editor_test.rb b/test/integration/clubsite_event_editor_test.rb new file mode 100644 index 0000000..5234964 --- /dev/null +++ b/test/integration/clubsite_event_editor_test.rb @@ -0,0 +1,373 @@ +require 'test_helper' + +class ClubsiteEventEditorTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + setup do + host! 'cluba.test' + end + + # --- Edit form --- + + test "edit form renders for the event's organizer" do + sign_in users(:organizer) + get "/events/edit/#{events(:cluba_race).id}" + assert_response :success + assert_select 'h1', text: 'Edit Event' + assert_select "form[action=?]", "/events/edit/#{events(:cluba_race).id}" + end + + test "edit form renders for an executive and seeds the JSON blobs" do + sign_in users(:executive) + get "/events/edit/#{events(:cluba_race).id}" + assert_response :success + organizers = JSON.parse(css_select('#EventOrganizers').first['value']) + assert_equal [users(:organizer).id], organizers.map { |organizer| organizer['id'] } + assert_equal roles(:organizer).id, organizers.first['role']['id'] + courses = JSON.parse(css_select('#EventCourses').first['value']) + assert_equal ['Long Course', 'Score Course'], courses.map { |course| course['name'] }.sort + assert_equal true, courses.find { |course| course['name'] == 'Score Course' }['isScoreO'] + end + + test "edit form redirects a plain member" do + sign_in users(:member) + get "/events/edit/#{events(:cluba_race).id}" + assert_response :redirect + assert_redirected_to '/' + end + + test "add form requires the edit privilege" do + sign_in users(:member) + get '/events/edit' + assert_response :redirect + + sign_in users(:executive) + get '/events/edit' + assert_response :success + assert_select 'h1', text: 'Add Event' + end + + test "series options are limited to current series unless the event's series is not current" do + old_series = Series.create!(name: 'Old Series', club: clubs(:cluba), is_current: false) + sign_in users(:executive) + + get "/events/edit/#{events(:cluba_race).id}" + assert_select '#EventSeriesId option', text: 'Old Series', count: 0 + + events(:cluba_race).update_columns(series_id: old_series.id) + get "/events/edit/#{events(:cluba_race).id}" + assert_select '#EventSeriesId option', text: 'Old Series' + assert_select '#EventSeriesId option', text: 'Wednesday Evening Series' + end + + # --- Saving --- + + test "create assembles dates in the club's time zone and stores UTC" do + sign_in users(:executive) + assert_difference 'Event.count', 1 do + post '/events/edit', params: { + event: { + name: 'New Sprint', + event_classification_id: event_classifications(:local).id, + date: '2025-06-18', time: '10:00', + finish_date: '2025-06-18', finish_time: '12:30', + deadline_date: '2025-06-17', deadline_time: '', + lat: clubs(:cluba).lat.to_s, lng: clubs(:cluba).lng.to_s, + organizers: '[]', courses: '[]' + } + } + end + event = Event.order(:id).last + assert_redirected_to "/events/view/#{event.id}" + assert_equal clubs(:cluba).id, event.club_id + # 10:00 Pacific in June is 17:00 UTC (PDT) + assert_equal Time.utc(2025, 6, 18, 17, 0), event.date.utc + assert_equal Time.utc(2025, 6, 18, 19, 30), event.read_attribute(:finish_date).utc + # Blank deadline time defaults to 23:59 club time + assert_equal Time.utc(2025, 6, 18, 6, 59), event.registration_deadline.utc + # The club's default marker position is not stored + assert_nil event.lat + assert_nil event.lng + end + + test "create in winter stores the standard-time UTC offset" do + sign_in users(:executive) + post '/events/edit', params: { + event: { + name: 'Winter Event', + event_classification_id: event_classifications(:local).id, + date: '2025-12-10', time: '10:00', + organizers: '[]', courses: '[]' + } + } + event = Event.order(:id).last + # 10:00 Pacific in December is 18:00 UTC (PST) + assert_equal Time.utc(2025, 12, 10, 18, 0), event.date.utc + end + + test "organizers blob creates rows and a re-save replaces them" do + sign_in users(:executive) + event = events(:cluba_race) + blob = [ + { id: users(:member).id, name: users(:member).name, + role: { id: roles(:course_planner).id, name: roles(:course_planner).name } } + ].to_json + post "/events/edit/#{event.id}", params: { + event: base_event_params(event).merge(organizers: blob, courses: '[]') + } + assert_redirected_to "/events/view/#{event.id}" + assert_equal [[users(:member).id, roles(:course_planner).id]], + event.organizers.reload.map { |organizer| [organizer.user_id, organizer.role_id] } + + # Saving again with a different blob replaces the rows wholesale + blob = [ + { id: users(:organizer).id, name: users(:organizer).name, + role: { id: roles(:organizer).id, name: roles(:organizer).name } } + ].to_json + post "/events/edit/#{event.id}", params: { + event: base_event_params(event).merge(organizers: blob, courses: '[]') + } + assert_equal [[users(:organizer).id, roles(:organizer).id]], + event.organizers.reload.map { |organizer| [organizer.user_id, organizer.role_id] } + end + + test "courses blob updates existing courses by id and adds new ones without deleting omitted ones" do + sign_in users(:organizer) + event = events(:cluba_race) + blob = [ + { id: courses(:race_long).id, name: 'Longer Course', description: 'Updated', + distance: 6000, climb: 200, isScoreO: false }, + { id: nil, name: 'Beginner Course', description: 'New', distance: 2000, climb: 50, isScoreO: true } + ].to_json + assert_difference 'event.courses.count', 1 do + post "/events/edit/#{event.id}", params: { + event: base_event_params(event).merge(organizers: '[]', courses: blob) + } + end + course = courses(:race_long).reload + assert_equal 'Longer Course', course.name + assert_equal 6000, course.distance + assert_equal 200, course.climb + new_course = event.courses.find_by(name: 'Beginner Course') + assert new_course.is_score_o + # The omitted score course survives (deletion happens via /courses/delete) + assert Course.exists?(courses(:race_score).id) + end + + test "save rejects invalid data and re-renders the form" do + sign_in users(:executive) + event = events(:cluba_race) + post "/events/edit/#{event.id}", params: { + event: base_event_params(event).merge(name: '', organizers: '[]', courses: '[]') + } + assert_response :success + assert_select 'h1', text: 'Edit Event' + assert_equal 'Spring Sprint', event.reload.name + end + + # --- Delete --- + + test "delete requires the delete privilege (level 90)" do + event = events(:cluba_race) + course_id = courses(:race_long).id + organizer_id = organizers(:oscar_on_race).id + sign_in users(:executive) + post "/events/delete/#{event.id}" + assert_redirected_to '/' + assert Event.exists?(event.id) + + sign_in users(:webmaster) + post "/events/delete/#{event.id}" + assert_redirected_to '/events/' + assert_not Event.exists?(event.id) + assert_not Course.exists?(course_id) + assert_not Organizer.exists?(organizer_id) + end + + # --- Planner --- + + test "planner redirects a plain member" do + sign_in users(:member) + get '/events/planner' + assert_response :redirect + assert_redirected_to '/' + end + + test "planner renders maps and volunteers for an executive" do + travel_to Time.utc(2025, 7, 1, 12) do + sign_in users(:executive) + get '/events/planner' + assert_response :success + assert_select 'h1', text: 'Event Planner' + assert_select 'td a[href=?]', "/maps/view/#{maps(:cluba_forest).id}", text: 'Forest Map' + assert_select 'td', text: 'Never used' + assert_select 'h2', text: "Who's Next?" + end + end + + # --- Printable entries --- + + test "printableEntries renders the printable layout with per-course entries" do + get "/events/printableEntries/#{events(:cluba_race).id}" + assert_response :success + assert_match 'clubsite_printable', response.body + assert_select 'nav', count: 0 + assert_select 'h3', text: /Long Course \(2 participants\)/ + assert_select 'td nobr', text: 'Mary Member' + # Mary has a current-year membership with the club + travel_to Time.utc(2026, 6, 1) do + get "/events/printableEntries/#{events(:cluba_race).id}" + end + assert_select 'td', text: '✓' + end + + # --- Upload maps --- + + test "uploadMaps renders per-course upload forms for the organizer" do + sign_in users(:organizer) + get "/events/uploadMaps/#{events(:cluba_race).id}" + assert_response :success + assert_select "form[action=?]", "/courses/uploadMap/#{courses(:race_long).id}" + assert_select "form[action=?]", "/courses/uploadMap/#{courses(:race_score).id}" + end + + test "uploadMaps redirects a plain member" do + sign_in users(:member) + get "/events/uploadMaps/#{events(:cluba_race).id}" + assert_response :redirect + end + + # --- Results editor --- + + test "editResults renders the result editor for the organizer" do + sign_in users(:organizer) + get "/events/editResults/#{events(:cluba_race).id}" + assert_response :success + assert_select ".result-editor[data-event-id=?]", events(:cluba_race).id.to_s + assert_select "form[action=?]", "/events/editResults/#{events(:cluba_race).id}" + end + + test "editResults redirects a plain member" do + sign_in users(:member) + get "/events/editResults/#{events(:cluba_race).id}" + assert_response :redirect + end + + test "update_results saves times, defaults blank status to ok and nulls all-zero times" do + sign_in users(:organizer) + event = events(:cluba_race) + blob = [ + { id: courses(:race_long).id, is_score_o: false, results: [ + { id: results(:member_long).id, user: { id: users(:member).id }, + hours: '1', minutes: '02', seconds: '03', milliseconds: '000', + status: '', registrant_comment: '', official_comment: 'Nice run' }, + { id: results(:organizer_long).id, user: { id: users(:organizer).id }, + hours: '0', minutes: '00', seconds: '00', milliseconds: '000', + status: 'did_not_finish', registrant_comment: '', official_comment: '' }, + { user: { id: users(:executive).id }, + hours: '0', minutes: '45', seconds: '10', milliseconds: '500', + status: 'ok', registrant_comment: '', official_comment: '' } + ] } + ].to_json + post "/events/editResults/#{event.id}", params: { event: { courses: blob, results_posted: '1' } } + assert_redirected_to "/events/view/#{event.id}" + + member_result = results(:member_long).reload + assert_equal 3723.0, member_result.time_seconds + assert_equal :ok, member_result.status + assert_equal 'Nice run', member_result.official_comment + assert_nil member_result.registrant_comment + + assert_nil results(:organizer_long).reload.time_seconds + assert_equal :did_not_finish, results(:organizer_long).status + + new_result = Result.find_by(user_id: users(:executive).id, course_id: courses(:race_long).id) + assert_in_delta 2710.5, new_result.time_seconds, 0.001 + + assert event.reload.results_posted + end + + test "update_results saves score points and the course's score-o flag" do + sign_in users(:organizer) + event = events(:cluba_race) + blob = [ + { id: courses(:race_score).id, is_score_o: true, results: [ + { user: { id: users(:member).id }, hours: '0', minutes: '50', seconds: '00', + milliseconds: '000', status: 'ok', score_points: 120 } + ] } + ].to_json + post "/events/editResults/#{event.id}", params: { event: { courses: blob, results_posted: '1' } } + result = Result.find_by(user_id: users(:member).id, course_id: courses(:race_score).id) + assert_equal 120, result.score_points + assert courses(:race_score).reload.is_score_o + end + + test "update_results rejects a course belonging to another event" do + sign_in users(:organizer) + foreign_course = Course.create!(event: events(:cluba_training), name: 'Foreign Course') + blob = [ + { id: foreign_course.id, is_score_o: false, results: [ + { user: { id: users(:member).id }, hours: '1', minutes: '00', seconds: '00', + milliseconds: '000', status: 'ok' } + ] } + ].to_json + assert_no_difference 'Result.count' do + post "/events/editResults/#{events(:cluba_race).id}", params: { event: { courses: blob, results_posted: '1' } } + end + assert_redirected_to '/' + assert_not events(:cluba_race).reload.results_posted + end + + # --- Live results visibility --- + + test "toggle_live_results_visibility flips the live result list's visible flag" do + event = events(:cluba_race) + result_list = ResultList.create!(event: event, user: users(:organizer), + status: ResultList::LIVE_STATUS, data: '', + upload_time: Time.current, visible: false) + sign_in users(:organizer) + + post "/events/toggle_live_results_visibility/#{event.id}/true" + assert_redirected_to "/events/view/#{event.id}" + assert result_list.reload.visible + + post "/events/toggle_live_results_visibility/#{event.id}/false" + assert_not result_list.reload.visible + end + + test "toggle_live_results_visibility redirects a plain member" do + sign_in users(:member) + post "/events/toggle_live_results_visibility/#{events(:cluba_race).id}/true" + assert_redirected_to '/' + end + + # --- Tenancy --- + + test "another club's domain cannot edit this club's event" do + host! 'clubb.test' + sign_in users(:global_admin) + get "/events/edit/#{events(:cluba_race).id}" + assert_response :not_found + + post "/events/edit/#{events(:cluba_race).id}", + params: { event: { name: 'Hijacked', organizers: '[]', courses: '[]' } } + assert_response :not_found + + post "/events/delete/#{events(:cluba_race).id}" + assert_response :not_found + assert Event.exists?(events(:cluba_race).id) + end + + private + + # Minimal valid form fields for re-saving an existing event + def base_event_params(event) + { + name: event.name, + event_classification_id: event.event_classification_id, + series_id: event.series_id, + date: event.date.strftime('%Y-%m-%d'), + time: event.date.strftime('%H:%M') + } + end +end diff --git a/test/integration/clubsite_map_editor_test.rb b/test/integration/clubsite_map_editor_test.rb new file mode 100644 index 0000000..3f34248 --- /dev/null +++ b/test/integration/clubsite_map_editor_test.rb @@ -0,0 +1,180 @@ +require 'test_helper' + +class ClubsiteMapEditorTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + setup do + host! 'cluba.test' + end + + teardown do + # Remove any files written to the data volume during the test + [clubs(:cluba), clubs(:clubb)].each do |club| + FileUtils.rm_rf(File.join(Settings.dataFolder, club.id.to_s)) + end + end + + test 'the add form renders for an executive' do + sign_in users(:executive) + get '/maps/edit' + assert_response :success + assert_select 'h1', 'Add Map' + assert_select 'form[action="/maps/edit"][enctype="multipart/form-data"]' + assert_select 'input[name="map[name]"]' + # New maps default the marker to the club location and have no + # immediate-save update URL yet + assert_select ".draggable-marker-map:not([data-update-url])" + assert_select "#MapLat[value=?]", clubs(:cluba).lat.to_s + end + + test 'the edit form renders an existing map for an executive' do + map = maps(:cluba_forest) + sign_in users(:executive) + get "/maps/edit/#{map.id}" + assert_response :success + assert_select 'h1', 'Edit Map' + assert_select 'input[name="map[name]"][value=?]', 'Forest Map' + assert_select ".draggable-marker-map[data-update-url=?]", "/maps/update/#{map.id}" + end + + test 'the edit form redirects an unprivileged member' do + sign_in users(:member) + get '/maps/edit' + assert_redirected_to '/' + end + + test 'the edit form redirects signed out visitors' do + get "/maps/edit/#{maps(:cluba_forest).id}" + assert_redirected_to '/' + end + + test "another club's map cannot be edited through this club's site" do + host! 'clubb.test' + sign_in users(:executive) + get "/maps/edit/#{maps(:cluba_forest).id}" + assert_response :not_found + end + + test 'creating a map persists it scoped to the current club' do + sign_in users(:executive) + assert_difference -> { Map.count }, 1 do + post '/maps/edit', params: { + map: { name: 'New Map', scale: '15000', lat: '49.5', lng: '-123.5' } + } + end + map = Map.order(:id).last + assert_equal clubs(:cluba).id, map.club_id + assert_equal 'New Map', map.name + assert_equal 15_000, map.scale + assert_redirected_to "/maps/view/#{map.id}" + assert_equal 'The map has been updated.', flash[:success] + end + + test 'creating a map without a name re-renders the form' do + sign_in users(:executive) + assert_no_difference -> { Map.count } do + post '/maps/edit', params: { map: { name: '' } } + end + assert_response :success + assert_select 'h1', 'Add Map' + end + + test 'an unprivileged member cannot save a map' do + sign_in users(:member) + assert_no_difference -> { Map.count } do + post '/maps/edit', params: { map: { name: 'New Map' } } + end + assert_redirected_to '/' + end + + test 'updating with an image upload stores the original, thumbnails and banner' do + map = maps(:cluba_forest) + sign_in users(:executive) + post "/maps/edit/#{map.id}", params: { + map: { name: 'Forest Map', image: fixture_file_upload('logo.png', 'image/png') } + } + assert_redirected_to "/maps/view/#{map.id}" + assert_equal 'The map has been updated.', flash[:success] + + store = MediaStore.new(clubs(:cluba).id, 'Map') + assert store.exists?(map.id), 'expected the original upload' + assert store.exists?(map.id, 'image'), 'expected the normalized image' + # Scaled thumbnails plus HiDPI doubles, including the regenerated + # 60x60 banner crop + %w[400x600 800x1200 50x50 100x100 60x60 120x120].each do |size| + assert store.exists?(map.id, size), "expected #{size} thumbnail" + end + end + + test 'an upload with a disallowed extension is rejected' do + map = maps(:cluba_forest) + sign_in users(:executive) + post "/maps/edit/#{map.id}", params: { + map: { name: 'Forest Map', image: fixture_file_upload('style.css', 'text/css') } + } + assert_redirected_to "/maps/view/#{map.id}" + assert_match(/is not allowed/, flash[:danger]) + assert_not MediaStore.new(clubs(:cluba).id, 'Map').exists?(map.id) + end + + test 'the webmaster can delete a map, removing its files' do + map = maps(:cluba_forest) + sign_in users(:webmaster) + post "/maps/edit/#{map.id}", params: { + map: { image: fixture_file_upload('logo.png', 'image/png') } + } + store = MediaStore.new(clubs(:cluba).id, 'Map') + assert store.exists?(map.id) + + assert_difference -> { Map.count }, -1 do + post "/maps/delete/#{map.id}" + end + assert_redirected_to '/maps/' + assert_not store.exists?(map.id) + assert_not store.exists?(map.id, 'image') + assert_not store.exists?(map.id, '400x600') + end + + test 'an executive cannot delete a map' do + map = maps(:cluba_forest) + sign_in users(:executive) + assert_no_difference -> { Map.count } do + post "/maps/delete/#{map.id}" + end + assert_redirected_to '/' + end + + test "another club's map cannot be deleted through this club's site" do + host! 'clubb.test' + sign_in users(:webmaster) + assert_no_difference -> { Map.count } do + post "/maps/delete/#{maps(:cluba_forest).id}" + end + assert_response :not_found + end + + test 'update_location updates the coordinates for an executive' do + map = maps(:cluba_forest) + sign_in users(:executive) + post "/maps/update/#{map.id}/49.5/-123.45" + assert_response :success + map.reload + assert_in_delta 49.5, map.lat + assert_in_delta(-123.45, map.lng) + end + + test 'update_location redirects an unprivileged member' do + map = maps(:cluba_forest) + sign_in users(:member) + post "/maps/update/#{map.id}/49.5/-123.45" + assert_redirected_to '/' + assert_in_delta 49.31, map.reload.lat + end + + test "update_location 404s for another club's map" do + host! 'clubb.test' + sign_in users(:executive) + post "/maps/update/#{maps(:cluba_forest).id}/49.5/-123.45" + assert_response :not_found + end +end diff --git a/test/integration/clubsite_registration_test.rb b/test/integration/clubsite_registration_test.rb new file mode 100644 index 0000000..0fd6beb --- /dev/null +++ b/test/integration/clubsite_registration_test.rb @@ -0,0 +1,269 @@ +require 'test_helper' + +class ClubsiteRegistrationTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + setup do + host! 'cluba.test' + end + + teardown do + # Remove any files written to the data volume during the test + [clubs(:cluba), clubs(:clubb)].each do |club| + FileUtils.rm_rf(File.join(Settings.dataFolder, club.id.to_s)) + end + end + + # --- Registration --- + + test 'a signed-in user can register themselves' do + sign_in users(:executive) + course = courses(:race_score) + assert_difference -> { Result.count }, 1 do + post "/courses/register/#{course.id}/0" + end + assert_redirected_to "/events/view/#{course.event_id}" + result = Result.find_by(course_id: course.id, user_id: users(:executive).id) + assert_equal users(:executive).id, result.registrant_id + end + + test 'registering someone else records the current user as the registrant' do + sign_in users(:executive) + course = courses(:race_score) + assert_difference -> { Result.count }, 1 do + post "/courses/register/#{course.id}/#{users(:member).id}" + end + assert_redirected_to "/events/view/#{course.event_id}" + result = Result.find_by(course_id: course.id, user_id: users(:member).id) + assert_equal users(:executive).id, result.registrant_id + end + + test 'registering a freshly added fake user' do + sign_in users(:member) + post '/users/add', params: { userName: 'Nora Newcomer' } + assert_response :success + new_user_id = JSON.parse(response.body) + new_user = User.find(new_user_id) + assert_equal 'Nora Newcomer', new_user.name + assert_nil new_user.email + + course = courses(:race_score) + assert_difference -> { Result.count }, 1 do + post "/courses/register/#{course.id}/#{new_user_id}" + end + assert_redirected_to "/events/view/#{course.event_id}" + result = Result.find_by(course_id: course.id, user_id: new_user_id) + assert_equal users(:member).id, result.registrant_id + end + + test 'a duplicate registration is a silent no-op' do + sign_in users(:member) + course = courses(:race_long) + assert_no_difference -> { Result.count } do + post "/courses/register/#{course.id}" + end + assert_redirected_to "/events/view/#{course.event_id}" + assert_nil flash[:success] + end + + test 'registration requires sign-in' do + assert_no_difference -> { Result.count } do + post "/courses/register/#{courses(:race_long).id}" + end + assert_redirected_to '/users/login' + end + + # --- Unregistration --- + + test 'a user can unregister themselves' do + sign_in users(:member) + course = courses(:race_long) + own_result_id = results(:member_long).id + other_result_id = results(:organizer_long).id + assert_difference -> { Result.count }, -1 do + post "/courses/unregister/#{course.id}" + end + assert_redirected_to "/events/view/#{course.event_id}" + assert_not Result.exists?(own_result_id) + # The other result on the course is untouched + assert Result.exists?(other_result_id) + end + + test 'you can unregister someone whose registration you made' do + # The member registered the organizer (organizer_long.registrant is member) + sign_in users(:member) + course = courses(:race_long) + result_id = results(:organizer_long).id + assert_difference -> { Result.count }, -1 do + post "/courses/unregister/#{course.id}/#{users(:organizer).id}" + end + assert_redirected_to "/events/view/#{course.event_id}" + assert_not Result.exists?(result_id) + end + + test 'you cannot unregister someone you did not register' do + sign_in users(:executive) + assert_no_difference -> { Result.count } do + post "/courses/unregister/#{courses(:race_long).id}/#{users(:member).id}" + end + assert_redirected_to '/' + end + + test 'unregistration requires sign-in' do + assert_no_difference -> { Result.count } do + post "/courses/unregister/#{courses(:race_long).id}" + end + assert_redirected_to '/users/login' + end + + # --- Course deletion --- + + test 'an organizer can delete a course along with its results' do + sign_in users(:organizer) + course = courses(:race_long) + assert_difference({ -> { Course.count } => -1, -> { Result.count } => -2 }) do + post "/courses/delete/#{course.id}" + end + assert_response :success + end + + test 'an unprivileged member cannot delete a course' do + sign_in users(:member) + assert_no_difference -> { Course.count } do + post "/courses/delete/#{courses(:race_long).id}" + end + assert_redirected_to '/' + end + + test 'course deletion redirects signed out visitors' do + assert_no_difference -> { Course.count } do + post "/courses/delete/#{courses(:race_long).id}" + end + assert_redirected_to '/' + end + + # --- Course map upload --- + + test 'an organizer can upload a course map' do + sign_in users(:organizer) + course = courses(:race_long) + post "/courses/uploadMap/#{course.id}", params: { file: fixture_file_upload('logo.png', 'image/png') } + assert_redirected_to "/events/uploadMaps/#{course.event_id}" + assert_equal 'Course map uploaded!', flash[:success] + assert MediaStore.new(clubs(:cluba).id, 'Course').exists?(course.id) + end + + test 'a disallowed file type flashes the store error and stores nothing' do + sign_in users(:organizer) + course = courses(:race_long) + post "/courses/uploadMap/#{course.id}", params: { file: fixture_file_upload('style.css', 'text/css') } + assert_redirected_to "/events/uploadMaps/#{course.event_id}" + assert_match(/not allowed/, flash[:alert]) + assert_not MediaStore.new(clubs(:cluba).id, 'Course').exists?(course.id) + end + + test 'uploading without a file flashes an error' do + sign_in users(:organizer) + course = courses(:race_long) + post "/courses/uploadMap/#{course.id}" + assert_redirected_to "/events/uploadMaps/#{course.event_id}" + assert_equal 'No file selected!', flash[:alert] + end + + test 'an unprivileged member cannot upload a course map' do + sign_in users(:member) + course = courses(:race_long) + post "/courses/uploadMap/#{course.id}", params: { file: fixture_file_upload('logo.png', 'image/png') } + assert_redirected_to "/events/view/#{course.event_id}" + assert_not MediaStore.new(clubs(:cluba).id, 'Course').exists?(course.id) + end + + # --- Registrant comments --- + + test 'the registrant can edit the registrant comment' do + # The member registered the organizer's entry + sign_in users(:member) + result = results(:organizer_long) + post '/results/editRegistrantComment', + params: { result: { id: result.id, registrant_comment: 'Need a ride' } } + assert_redirected_to "/events/view/#{courses(:race_long).event_id}" + assert_equal 'Need a ride', result.reload.registrant_comment + end + + test 'the registered user can edit their own comment' do + sign_in users(:organizer) + result = results(:organizer_long) + post '/results/editRegistrantComment', + params: { result: { id: result.id, registrant_comment: 'Offering a ride' } } + assert_redirected_to "/events/view/#{courses(:race_long).event_id}" + assert_equal 'Offering a ride', result.reload.registrant_comment + end + + test 'a stranger cannot edit the comment' do + sign_in users(:executive) + result = results(:member_long) + post '/results/editRegistrantComment', + params: { result: { id: result.id, registrant_comment: 'nope' } } + assert_redirected_to '/' + assert_nil result.reload.registrant_comment + end + + test 'comment editing requires sign-in' do + result = results(:member_long) + post '/results/editRegistrantComment', + params: { result: { id: result.id, registrant_comment: 'nope' } } + assert_redirected_to '/users/login' + assert_nil result.reload.registrant_comment + end + + # --- Result deletion --- + + test 'an organizer can delete a result' do + sign_in users(:organizer) + assert_difference -> { Result.count }, -1 do + post "/results/delete/#{results(:member_long).id}" + end + assert_response :success + end + + test 'an unprivileged member cannot delete a result' do + sign_in users(:member) + assert_no_difference -> { Result.count } do + post "/results/delete/#{results(:member_long).id}" + end + assert_redirected_to '/' + end + + # --- Tenancy --- + + test "acting on another club's data 404s" do + host! 'clubb.test' + + # A request that 404s does not persist the test sign-in, so sign in + # before each request. + assert_no_difference -> { Result.count } do + sign_in users(:admin) + post "/courses/register/#{courses(:race_long).id}" + assert_response :not_found + + sign_in users(:admin) + post "/courses/unregister/#{courses(:race_long).id}" + assert_response :not_found + + sign_in users(:admin) + post "/results/delete/#{results(:member_long).id}" + assert_response :not_found + end + + assert_no_difference -> { Course.count } do + sign_in users(:admin) + post "/courses/delete/#{courses(:race_long).id}" + assert_response :not_found + end + + sign_in users(:admin) + post "/courses/uploadMap/#{courses(:race_long).id}", + params: { file: fixture_file_upload('logo.png', 'image/png') } + assert_response :not_found + end +end diff --git a/test/integration/clubsite_users_admin_test.rb b/test/integration/clubsite_users_admin_test.rb new file mode 100644 index 0000000..4619fb4 --- /dev/null +++ b/test/integration/clubsite_users_admin_test.rb @@ -0,0 +1,184 @@ +require 'test_helper' + +class ClubsiteUsersAdminTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + setup do + host! 'cluba.test' + end + + # --- Autocomplete --- + + test 'autocomplete requires sign-in' do + get '/users/index.json', params: { term: 'Mary' } + assert_response :unauthorized + end + + test 'autocomplete filters by term and returns the expected shape' do + sign_in users(:member) + get '/users/index.json', params: { term: 'Mary' } + assert_response :success + entries = JSON.parse(response.body) + assert_equal 1, entries.length + assert_equal( + { 'name' => 'Mary Member', 'identifiableName' => 'Mary Member', 'id' => users(:member).id }, + entries.first + ) + end + + test 'autocomplete without a term returns an empty list' do + sign_in users(:member) + get '/users/index.json' + assert_response :success + assert_equal [], JSON.parse(response.body) + end + + test 'autocomplete allowFake=false hides users without an email' do + sign_in users(:member) + fake = User.create_fake('Frankie Fake') + + get '/users/index.json', params: { term: 'Frankie' } + assert_includes JSON.parse(response.body).map { |entry| entry['id'] }, fake.id + + get '/users/index.json', params: { term: 'Frankie', allowFake: 'false' } + assert_equal [], JSON.parse(response.body) + end + + # --- Adding fake users --- + + test 'add creates a fake user and returns its id' do + sign_in users(:member) + assert_difference -> { User.count }, 1 do + post '/users/add', params: { userName: 'Paula Person' } + end + assert_response :success + user = User.find(JSON.parse(response.body)) + assert_equal 'Paula Person', user.name + assert_nil user.email + assert_not user.has_password? + end + + test 'add requires sign-in' do + assert_no_difference -> { User.count } do + post '/users/add', params: { userName: 'Paula Person' } + end + assert_redirected_to '/users/login' + end + + # --- Merging --- + + test 'merge re-points associations, fills blank target fields and destroys the source' do + sign_in users(:webmaster) + target = users(:member) + source = User.create_fake('Mary Member') + source.update_columns(year_of_birth: 1980, si_number: 123_456, referred_from: 'a friend', + club_id: clubs(:clubb).id) + membership = Membership.create!(user_id: source.id, club: clubs(:clubb), year: 2024) + organizer_row = Organizer.create!(user_id: source.id, event: events(:cluba_training), role: roles(:organizer)) + result_row = Result.create!(user_id: source.id, course: courses(:race_score), registrant_id: source.id) + privilege_row = Privilege.create!(user_id: source.id, user_group: groups(:cluba_executive)) + + assert_difference -> { User.count }, -1 do + post "/users/merge/#{target.id}/#{source.id}" + end + assert_redirected_to '/users/showDuplicates' + assert_not User.exists?(source.id) + + target.reload + # Blank target fields are filled from the source + assert_equal 1980, target.year_of_birth + assert_equal 123_456, target.si_number + assert_equal 'a friend', target.referred_from + # Populated target fields win + assert_equal 'member@example.com', target.email + assert_equal clubs(:cluba).id, target.club_id + + # Associated rows now point at the target + assert_equal target.id, membership.reload.user_id + assert_equal target.id, organizer_row.reload.user_id + assert_equal target.id, result_row.reload.user_id + assert_equal target.id, result_row.registrant_id + assert_equal target.id, privilege_row.reload.user_id + end + + test 'merge requires the merge privilege' do + sign_in users(:executive) + source = User.create_fake('Mary Member') + assert_no_difference -> { User.count } do + post "/users/merge/#{users(:member).id}/#{source.id}" + end + assert_redirected_to '/' + assert User.exists?(source.id) + end + + test 'merge requires sign-in' do + source = User.create_fake('Mary Member') + assert_no_difference -> { User.count } do + post "/users/merge/#{users(:member).id}/#{source.id}" + end + assert_redirected_to '/users/login' + end + + test 'the merge privilege is club-scoped' do + host! 'clubb.test' + sign_in users(:webmaster) + source = User.create_fake('Mary Member') + assert_no_difference -> { User.count } do + post "/users/merge/#{users(:member).id}/#{source.id}" + end + assert_redirected_to '/' + end + + # --- Show duplicates --- + + test 'showDuplicates requires the merge privilege' do + sign_in users(:executive) + get '/users/showDuplicates' + assert_redirected_to '/' + end + + test 'showDuplicates requires sign-in' do + get '/users/showDuplicates' + assert_redirected_to '/users/login' + end + + test 'showDuplicates lists detected duplicates with merge buttons' do + sign_in users(:webmaster) + duplicate = User.create_fake('Mary Member') + get '/users/showDuplicates' + assert_response :success + assert_select 'h1', 'Merge user accounts' + # The member has results at a cluba event, so they are the primary + assert_select "form[action='/users/merge/#{users(:member).id}/#{duplicate.id}']" + # The webmaster (90) is below the user edit level, so no manual merge form + assert_select "select[name='user[0][user_id]']", count: 0 + end + + test 'an admin sees the manual merge form and can merge two picked users' do + sign_in users(:admin) + duplicate = User.create_fake('Extra Person') + target = users(:member) + + get '/users/showDuplicates' + assert_response :success + assert_select "select[name='user[0][user_id]']" + assert_select "select[name='user[1][user_id]']" + + assert_difference -> { User.count }, -1 do + post '/users/showDuplicates', + params: { user: { '0' => { user_id: target.id }, '1' => { user_id: duplicate.id } } } + end + assert_redirected_to '/users/showDuplicates' + assert_not User.exists?(duplicate.id) + end + + test 'picking the same user twice does not merge' do + sign_in users(:admin) + assert_no_difference -> { User.count } do + post '/users/showDuplicates', + params: { user: { '0' => { user_id: users(:member).id }, '1' => { user_id: users(:member).id } } } + end + assert_redirected_to '/users/showDuplicates' + assert User.exists?(users(:member).id) + end +end From f21654269af45467e06187f811c53737fe2eb9a6 Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Thu, 30 Jul 2026 22:55:33 +0200 Subject: [PATCH 07/16] Document club website serving in the project guide --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 578e42f..86cd3d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -WhyJustRun Core is a Ruby on Rails application providing authentication, IOF XML APIs, and cross-club pages for [whyjustrun.ca](https://whyjustrun.ca). It works alongside the separate [WhyJustRun Clubsite](https://github.com/WhyJustRun/Clubsite) app. +WhyJustRun Core is a Ruby on Rails application serving [whyjustrun.ca](https://whyjustrun.ca) (authentication, IOF XML APIs, cross-club pages) and every club's website. Club domains are resolved from the Host header into a `Clubsite::` controller namespace (`app/controllers/clubsite/base_controller.rb`); requests on club domains run in the club's timezone and club-scoped data is always accessed through `current_club` associations. The legacy [WhyJustRun Clubsite](https://github.com/WhyJustRun/Clubsite) (CakePHP) is being decommissioned; until cutover it still serves club domains in production against the same database, so cross-app SSO endpoints and permissive CORS must not be removed from master (they are removed on the `rails-cutover` and `post-cutover-cleanup` branches). ## Development Commands From 3190dd6c167f04c3bea2821945abb76f893e0cd2 Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Fri, 31 Jul 2026 07:26:53 +0200 Subject: [PATCH 08/16] Accept PATCH on the legacy edit routes form_with on a persisted record submits with the PATCH method override, but the legacy-shaped edit endpoints only accepted POST, so the edit forms for clubs, maps, series, roles, map standards, memberships, and officials 404ed in the browser (tests had been posting directly). Regression tests now submit the way browsers do. --- config/routes.rb | 16 ++--- .../integration/clubsite_form_methods_test.rb | 58 +++++++++++++++++++ 2 files changed, 66 insertions(+), 8 deletions(-) create mode 100644 test/integration/clubsite_form_methods_test.rb diff --git a/config/routes.rb b/config/routes.rb index a39567a..3f64c96 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -53,7 +53,7 @@ get 'events/planner', to: 'events#planner' get 'events/printableEntries/:id', to: 'events#printable_entries' get 'events/edit(/:id)', to: 'events#edit' - post 'events/edit(/:id)', to: 'events#save' + match 'events/edit(/:id)', to: 'events#save', via: [:post, :patch] post 'events/delete/:id', to: 'events#destroy' get 'events/uploadMaps/:id', to: 'events#upload_maps' get 'events/editResults/:id', to: 'events#edit_results' @@ -67,7 +67,7 @@ get 'maps/download/:id', to: 'maps#download' get 'maps/rendering/:id(/:thumbnail)', to: 'maps#rendering' get 'maps/edit(/:id)', to: 'maps#edit' - post 'maps/edit(/:id)', to: 'maps#save' + match 'maps/edit(/:id)', to: 'maps#save', via: [:post, :patch] post 'maps/delete/:id', to: 'maps#destroy' post 'maps/update/:id/:lat/:lng', to: 'maps#update_location', constraints: { lat: /-?[\d.]+/, lng: /-?[\d.]+/ } @@ -104,31 +104,31 @@ # Club admin get 'clubs(/index)', to: 'clubs#index' get 'clubs/edit', to: 'clubs#edit' - post 'clubs/edit', to: 'clubs#update' + match 'clubs/edit', to: 'clubs#update', via: [:post, :patch] get 'series(/index)', to: 'series#index', as: :clubsite_series get 'series/edit(/:id)', to: 'series#edit' - post 'series/edit(/:id)', to: 'series#update' + match 'series/edit(/:id)', to: 'series#update', via: [:post, :patch] get 'roles(/index)', to: 'roles#index', as: :clubsite_roles get 'roles/edit(/:id)', to: 'roles#edit' - post 'roles/edit(/:id)', to: 'roles#update' + match 'roles/edit(/:id)', to: 'roles#update', via: [:post, :patch] get 'mapStandards(/index)', to: 'map_standards#index' get 'map_standards(/index)', to: 'map_standards#index' get 'mapStandards/edit(/:id)', to: 'map_standards#edit' - post 'mapStandards/edit(/:id)', to: 'map_standards#update' + match 'mapStandards/edit(/:id)', to: 'map_standards#update', via: [:post, :patch] post 'mapStandards/delete/:id', to: 'map_standards#destroy' get 'memberships(/index)', to: 'memberships#index' get 'memberships/edit(/:id)', to: 'memberships#edit' - post 'memberships/edit(/:id)', to: 'memberships#update' + match 'memberships/edit(/:id)', to: 'memberships#update', via: [:post, :patch] post 'memberships/delete/:id', to: 'memberships#destroy' get 'officials(/index)', to: 'officials#index' post 'officials/add', to: 'officials#create' get 'officials/edit/:id', to: 'officials#edit' - post 'officials/edit/:id', to: 'officials#update' + match 'officials/edit/:id', to: 'officials#update', via: [:post, :patch] post 'officials/delete/:id', to: 'officials#destroy' get 'privileges(/index)', to: 'privileges#index' diff --git a/test/integration/clubsite_form_methods_test.rb b/test/integration/clubsite_form_methods_test.rb new file mode 100644 index 0000000..3d5677c --- /dev/null +++ b/test/integration/clubsite_form_methods_test.rb @@ -0,0 +1,58 @@ +require 'test_helper' + +# Browsers submit form_with(model:) for persisted records as PATCH via the +# _method override; the legacy-shaped edit routes must accept it as well as +# POST. +class ClubsiteFormMethodsTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + setup do + host! 'cluba.test' + sign_in users(:admin) + end + + test "map edit form submits via PATCH" do + patch "/maps/edit/#{maps(:cluba_forest).id}", params: { map: { name: 'Renamed Map' } } + assert_response :redirect + assert_equal 'Renamed Map', maps(:cluba_forest).reload.name + end + + test "club edit form submits via PATCH" do + patch '/clubs/edit', params: { club: { name: 'Renamed Club' } } + assert_response :redirect + assert_equal 'Renamed Club', clubs(:cluba).reload.name + end + + test "series edit form submits via PATCH" do + patch "/series/edit/#{series(:cluba_wednesday).id}", params: { series: { name: 'Renamed Series' } } + assert_response :redirect + assert_equal 'Renamed Series', series(:cluba_wednesday).reload.name + end + + test "role edit form submits via PATCH" do + patch "/roles/edit/#{roles(:organizer).id}", params: { role: { name: 'Renamed Role' } } + assert_response :redirect + assert_equal 'Renamed Role', roles(:organizer).reload.name + end + + test "map standard edit form submits via PATCH" do + map_standard = MapStandard.create!(name: 'Standard') + patch "/mapStandards/edit/#{map_standard.id}", params: { map_standard: { name: 'Renamed Standard' } } + assert_response :redirect + assert_equal 'Renamed Standard', map_standard.reload.name + end + + test "membership edit form submits via PATCH" do + membership = memberships(:cluba_member_current) + patch "/memberships/edit/#{membership.id}", params: { membership: { year: 2031 } } + assert_response :redirect + assert_equal 2031, membership.reload.year + end + + test "event edit form submits via PATCH" do + event = events(:cluba_training) + patch "/events/edit/#{event.id}", params: { event: { name: 'Renamed Event' } } + assert_response :redirect + assert_equal 'Renamed Event', event.reload.name + end +end From bdf6d1062958918369a95d3513867ca305887979 Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Fri, 31 Jul 2026 07:35:37 +0200 Subject: [PATCH 09/16] Add browser test plan --- doc/BROWSER_TESTING.md | 102 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 doc/BROWSER_TESTING.md diff --git a/doc/BROWSER_TESTING.md b/doc/BROWSER_TESTING.md new file mode 100644 index 0000000..d13512b --- /dev/null +++ b/doc/BROWSER_TESTING.md @@ -0,0 +1,102 @@ +# Browser test plan + +The 240 integration tests cover routing, authorization, tenancy, and data +behavior server-side, but none of the JavaScript-driven UI. This plan covers +the browser layer: what gets automated as system tests, what stays manual, +and the infrastructure needed. + +## Infrastructure (one-time setup) + +- **Stack**: Rails system tests (`test/system/`, `ActionDispatch::SystemTestCase`) + with Capybara + selenium-webdriver driving headless Chromium. Gems go in the + test group; the Docker dev image and CI runner need `chromium` + + `chromium-chromedriver`. +- **Multi-domain**: club fixtures use `cluba.test` domains. Chrome is launched + with `--host-resolver-rules=MAP *.test 127.0.0.1` so no DNS is needed; + `Capybara.app_host` switches between the apex and club domains per test. +- **Port handling (enabling change)**: `Clubsite::BaseController#set_current_club` + matches `request.host_with_port` against `clubs.domain`. The system-test + server runs on a random port, so the lookup must also try the portless + `request.host` (falling back exactly as production does, where domains have + no port). +- **Console-error tripwire**: a shared teardown assertion fails any system + test whose page produced severe browser console errors. This is the widest + net for catching de-AMD/wiring regressions in JS we don't otherwise assert + on. +- **CI**: a separate `test:system` job (system tests are slower and flakier + than integration tests; a separate job keeps the fast suite's signal + clean). If flakiness becomes a tax, the job can be made non-blocking while + keeping the local suite mandatory — but it starts as a required check. + +## P0 — automate first (the flows that would block cutover) + +Each of these exercises JS that the integration suite cannot reach. + +1. **SSO round trip**: club → Sign in link → apex form → back to club signed + in; sign out ends both sessions. (Backstops the token handoff in a real + browser with real cookies.) +2. **Event create + edit**: fill the form, add two courses and an organizer + through the Knockout editors (role select populated from `/roles/index.json`, + person picker autocomplete from `/users/index.json`), save, verify + persistence; re-edit and verify the editors rehydrate from the JSON blobs. +3. **Results editor**: open `editResults`, enter a time/status/score for an + existing registrant, add a new person (fake-user path), save, verify rows; + delete a result (exercises the `$.post` AJAX path). +4. **Course registration**: register self, register another person via the + register-others picker (creates a fake user), unregister; verify the + permission-driven button states. +5. **In-place editing**: jEditable content block on the home page and a + Resources page title/body — edit, save, reload, verify (exercises the + jEditable + CSRF token wiring). +6. **Map create/edit with image upload**: submit the multipart form, verify + thumbnails render on the map page; drag-update is simulated by posting the + marker JS's endpoint from the page context. +7. **Calendar page**: FullCalendar renders the seeded events from + `/club/:id/events.json`; month navigation updates the URL (pushState + path). +8. **Event listing + home event list**: the Knockout list populates from the + IOF XML feed (same-origin fetch + XML parsing path). +9. **Club settings + privileges admin**: edit club fields, grant and revoke a + privilege; verifies the admin forms end-to-end after the PATCH/POST fix. + +## P1 — automate second + +- Live results visibility toggle; registrant comment modal. +- Printable entries page (printable layout, member checkmarks). +- Series admin with the color picker; series colors appearing in the + calendar legend and list. +- Branding uploads (header image/logo/custom CSS) and their appearance in + the layout (srcset header, CSS override link last). +- User merge + duplicates screen. +- `.embed` iframe variants of the calendar and maps pages. +- Memberships, officials, map standards, roles admin CRUD through the real + forms. +- Password reset email flow (assert the mail, follow the link). + +## Not automated — manual checklist + +- **Visual fidelity vs the PHP site**: run both apps against the same dev + database (`localhost:3001` PHP club vs the Rails club) and compare the main + pages side by side. Automation can't judge "looks right". +- **Redactor**: licensed, not in the repo. With a copy in `public/redactor/`, + manually verify rich-text editing on event description + pages, and image + upload through the editor. (Automatable later if a licensed copy is + available to CI — keep it manual for now.) +- **Third-party embeds**: Google Maps rendering/draggable markers, Flickr + photos, Facebook box, Juicer feed — external services; spot-check manually. + Automated tests assert only the container markup and that no console + errors fire when keys are absent. +- **Cross-browser/mobile**: one manual pass in Safari and a phone-sized + viewport (navbar collapse, calendar touch). +- **Pre-cutover, against production**: the smoke script over every real + domain (see deploy/CUTOVER.md), then a manual pass of P0 flows 1-6 on one + real club with production data. + +## Sequencing + +1. Infrastructure + the console-error tripwire + P0 flows 1, 5, 7, 8 (pure + read/JS wiring — fast wins that would already have caught the PATCH bug's + class). +2. P0 flows 2, 3, 4, 6, 9 (editor-heavy). +3. P1 as time allows; anything still red-flagged by then stays on the manual + checklist. From 14e2d6bc0d7d6f3cd9c76f327635c7c94f37ca70 Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Fri, 31 Jul 2026 08:30:12 +0200 Subject: [PATCH 10/16] Add browser system test infrastructure - Capybara + selenium driving headless Chromium (installed in the image and CI); the system chromedriver is used explicitly since Selenium Manager hangs in the container, and --disable-gpu is required for Chrome to start at all - All hostnames resolve to the test server so the multi-domain club fixtures work in a real browser; club lookup falls back to the portless host since the test server picks a random port - Every system test fails on unexpected browser console errors, the widest net for JS wiring regressions; expected third-party noise is ignored - Club pages now fetch the event and result feeds same-origin instead of via the absolute apex URL (they are served by this app now), which the console tripwire caught - Separate CI job; the image push depends on it --- .github/workflows/ci.yml | 55 +++++++++++++- Dockerfile | 2 + Gemfile | 5 ++ Gemfile.lock | 27 +++++++ app/controllers/clubsite/base_controller.rb | 10 +-- app/views/clubsite/events/_list.html.erb | 2 +- .../events/_live_result_list.html.erb | 2 +- .../clubsite/events/_results_list.html.erb | 2 +- app/views/layouts/clubsite/_head.html.erb | 3 +- test/application_system_test_case.rb | 71 +++++++++++++++++++ test/integration/clubsite_events_test.rb | 4 +- test/system/smoke_test.rb | 13 ++++ 12 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 test/application_system_test_case.rb create mode 100644 test/system/smoke_test.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd781c3..e29e485 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,59 @@ jobs: - name: Run tests run: bundle exec rails test + system-test: + runs-on: ubuntu-latest + + services: + db: + image: mariadb:10.11 + env: + MARIADB_ROOT_PASSWORD: root + MARIADB_DATABASE: whyjustrun-test + ports: + - 3306:3306 + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=5s + --health-timeout=5s + --health-retries=10 + + env: + RAILS_ENV: test + WJR_DATABASE_NAME: whyjustrun + WJR_DATABASE_TEST_NAME: whyjustrun-test + WJR_DATABASE_HOST: 127.0.0.1 + WJR_DATABASE_USERNAME: root + WJR_DATABASE_PASSWORD: root + WJR_PASSWORD_SALT: test-only-salt + WJR_SECRET_BASE: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_SECRET_TOKEN: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_DEVISE_SECRET_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef + WJR_RECAPTCHA_SITE_KEY: test + WJR_RECAPTCHA_SECRET_KEY: test + WJR_SMTP_ADDRESS: localhost + WJR_SMTP_DOMAIN: example.com + WJR_SMTP_USER_NAME: test + WJR_SMTP_PASSWORD: test + WJR_DATA_FOLDER: /tmp/whyjustrun-data/ + WJR_DATA_URL: http://localhost/data/ + + steps: + - uses: actions/checkout@v4 + + - name: Install MySQL client headers + run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev imagemagick ghostscript + + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Prepare test database + run: bundle exec rails db:test:prepare + + - name: Run system tests + run: bundle exec rails test:system + brakeman: runs-on: ubuntu-latest steps: @@ -80,7 +133,7 @@ jobs: push: runs-on: ubuntu-latest - needs: [test, brakeman] + needs: [test, system-test, brakeman] if: github.event_name == 'push' steps: diff --git a/Dockerfile b/Dockerfile index 3dcc894..c549ea3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,8 @@ RUN apk add --no-cache \ mariadb-dev \ imagemagick \ ghostscript \ + chromium \ + chromium-chromedriver \ bash # used in crontab generated by whenever RUN mkdir /application diff --git a/Gemfile b/Gemfile index 138485e..1678ed1 100644 --- a/Gemfile +++ b/Gemfile @@ -62,3 +62,8 @@ gem 'haversine', '~> 0.3.2' group :development, :test do gem 'brakeman', require: false end + +group :test do + gem 'capybara' + gem 'selenium-webdriver' +end diff --git a/Gemfile.lock b/Gemfile.lock index 5a9f04a..dfff5f5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -81,6 +81,8 @@ GEM minitest (>= 5.1, < 6) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) autoprefixer-rails (10.4.19.0) execjs (~> 2) base64 (0.3.0) @@ -93,6 +95,15 @@ GEM brakeman (8.0.5) racc builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) cgi (0.4.2) chronic (0.10.2) coffee-rails (5.0.0) @@ -170,6 +181,7 @@ GEM net-pop net-smtp marcel (1.1.0) + matrix (0.4.3) mini_magick (5.3.2) logger mini_mime (1.1.5) @@ -203,6 +215,7 @@ GEM psych (5.3.1) date stringio + public_suffix (7.0.5) puma (6.4.3) nio4r (~> 2.0) pundit (2.3.2) @@ -253,14 +266,17 @@ GEM psych (>= 4.0.0) tsort recaptcha (5.17.0) + regexp_parser (2.12.0) reline (0.6.3) io-console (~> 0.5) responders (3.2.0) actionpack (>= 7.0) railties (>= 7.0) + rexml (3.4.4) ruby-vips (2.3.0) ffi (~> 1.12) logger + rubyzip (3.4.1) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) sassc (2.4.0) @@ -272,6 +288,12 @@ GEM sprockets-rails tilt securerandom (0.4.1) + selenium-webdriver (4.46.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 4.0) + websocket (~> 1.0) sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) @@ -293,11 +315,14 @@ GEM useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) + websocket (1.2.11) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) whenever (1.0.0) chronic (>= 0.6.3) + xpath (3.2.0) + nokogiri (~> 1.8) zeitwerk (2.7.5) PLATFORMS @@ -309,6 +334,7 @@ DEPENDENCIES bcrypt (~> 3.1.22) bootstrap-sass (~> 3.4.1) brakeman + capybara coffee-rails (~> 5.0.0) coffee-script (~> 2.4.1) config (~> 5.5.1) @@ -330,6 +356,7 @@ DEPENDENCIES rails (~> 7.1) recaptcha (~> 5.17.0) sass-rails (~> 6.0.0) + selenium-webdriver tzinfo-data uglifier (~> 4.2.0) whenever (~> 1.0.0) diff --git a/app/controllers/clubsite/base_controller.rb b/app/controllers/clubsite/base_controller.rb index 5235244..d2d49cb 100644 --- a/app/controllers/clubsite/base_controller.rb +++ b/app/controllers/clubsite/base_controller.rb @@ -16,12 +16,14 @@ class BaseController < ApplicationController private def set_current_club - # Ports include the port so development domains like demo.localhost:3000 work. - requested_host = request.host_with_port - @current_club = Club.find_by(domain: requested_host) + # Match with the port first so development domains like + # demo.localhost:3000 work, then without it (production domains carry no + # port, whatever port the server actually runs on). + @current_club = Club.find_by(domain: request.host_with_port) || Club.find_by(domain: request.host) return if @current_club.present? - redirected = Club.find_by(redirect_domain: requested_host) + redirected = Club.find_by(redirect_domain: request.host_with_port) || + Club.find_by(redirect_domain: request.host) if redirected redirect_to "#{redirected.domain_protocol}://#{redirected.domain}#{request.fullpath}", status: :moved_permanently, allow_other_host: true diff --git a/app/views/clubsite/events/_list.html.erb b/app/views/clubsite/events/_list.html.erb index 17cce24..0e4e069 100644 --- a/app/views/clubsite/events/_list.html.erb +++ b/app/views/clubsite/events/_list.html.erb @@ -13,7 +13,7 @@ data-event-list-type="time-window" data-event-list-older-button="#older-button" data-event-list-newer-button="#newer-button" - data-event-list-url="<%= core_url("/club/#{current_club.id}/events.xml?iof_version=3.0&external_significant_events=all") %>"> + data-event-list-url="/club/<%= current_club.id %>/events.xml?iof_version=3.0&external_significant_events=all">

    Showing events after

    Showing events before

    diff --git a/app/views/clubsite/events/_live_result_list.html.erb b/app/views/clubsite/events/_live_result_list.html.erb index 218b0ad..3e913fb 100644 --- a/app/views/clubsite/events/_live_result_list.html.erb +++ b/app/views/clubsite/events/_live_result_list.html.erb @@ -6,7 +6,7 @@

    -
    " data-result-list-mode="live"> +
    <%= render 'knockout_result_list', mode: 'live' %>
    diff --git a/app/views/clubsite/events/_results_list.html.erb b/app/views/clubsite/events/_results_list.html.erb index c8d15dc..de964d8 100644 --- a/app/views/clubsite/events/_results_list.html.erb +++ b/app/views/clubsite/events/_results_list.html.erb @@ -1,6 +1,6 @@ <%# locals: event_id %>
    -
    "> +
    <%= render 'knockout_result_list', mode: 'normal' %>
    diff --git a/app/views/layouts/clubsite/_head.html.erb b/app/views/layouts/clubsite/_head.html.erb index 9d5d21c..13fd974 100644 --- a/app/views/layouts/clubsite/_head.html.erb +++ b/app/views/layouts/clubsite/_head.html.erb @@ -1,6 +1,7 @@ - +<%# Empty: the core APIs are same-origin now that club sites are served by this app %> + <%= csrf_meta_tags %> diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 0000000..f750d82 --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,71 @@ +require 'test_helper' + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + CHROMIUM_BIN = '/usr/bin/chromium-browser'.freeze + CHROMEDRIVER = '/usr/bin/chromedriver'.freeze + + # Use the system chromedriver when present (the Docker image installs a + # matching chromium + chromedriver pair); otherwise Selenium Manager picks + # one, which hangs inside the container. + driver_options = { screen_size: [1400, 1400] } + if File.exist?(CHROMEDRIVER) + driver_options[:options] = { service: ::Selenium::WebDriver::Chrome::Service.new(path: CHROMEDRIVER) } + end + + driven_by :selenium, using: :headless_chrome, **driver_options do |options| + # Club fixture domains use .test; resolve them all to the local server + options.add_argument('--host-resolver-rules=MAP * 127.0.0.1') + options.add_argument('--no-sandbox') + # Without this the GPU process hangs Chrome startup inside the container + options.add_argument('--disable-gpu') + options.add_argument('--disable-dev-shm-usage') + options.add_option('goog:loggingPrefs', { browser: 'ALL' }) + options.binary = CHROMIUM_BIN if File.exist?(CHROMIUM_BIN) + end + + Capybara.always_include_port = true + + # Browser console errors almost always mean broken JS wiring; fail loudly. + # Individual tests can allow expected noise via allow_console_errors(/pattern/). + # Third-party hosts are unreachable in tests (everything resolves to the + # local server), so their load failures are expected noise. + IGNORED_CONSOLE_ERRORS = [ + %r{/favicon\.ico}, %r{/apple-touch-icon}, %r{/redactor/}, + /Google Maps/i, /maps\.googleapis/, /flickr/i, + /facebook/i, /juicer/i, /oss\.maxcdn\.com/ + ].freeze + + teardown do + raise_on_console_errors if passed? + end + + def allow_console_errors(pattern) + (@allowed_console_errors ||= []) << pattern + end + + def visit_club(path = '/', club: clubs(:cluba)) + visit "http://#{club.domain}#{path}" + end + + def visit_apex(path = '/') + visit "http://#{Settings.host}#{path}" + end + + def sign_in_through_browser(user, password: 'password123') + user.update!(password: password, password_confirmation: password) + visit_apex '/users/sign_in' + fill_in 'user[email]', with: user.email + fill_in 'user[password]', with: password + click_button 'Log in' + end + + private + + def raise_on_console_errors + logs = page.driver.browser.logs.get(:browser) + ignored = IGNORED_CONSOLE_ERRORS + (@allowed_console_errors || []) + errors = logs.select { |entry| entry.level == 'SEVERE' } + .reject { |entry| ignored.any? { |pattern| entry.message =~ pattern } } + assert errors.empty?, "Browser console errors:\n#{errors.map(&:message).join("\n")}" + end +end diff --git a/test/integration/clubsite_events_test.rb b/test/integration/clubsite_events_test.rb index 40b663b..ec8123b 100644 --- a/test/integration/clubsite_events_test.rb +++ b/test/integration/clubsite_events_test.rb @@ -32,7 +32,7 @@ class ClubsiteEventsTest < ActionDispatch::IntegrationTest get '/events/listing' assert_response :success assert_select '.event-list[data-event-list-url=?]', - "#{Settings.coreURL.chomp('/')}/club/#{clubs(:cluba).id}/events.xml?iof_version=3.0&external_significant_events=all" + "/club/#{clubs(:cluba).id}/events.xml?iof_version=3.0&external_significant_events=all" assert_select 'script#event-box-template' end @@ -58,7 +58,7 @@ class ClubsiteEventsTest < ActionDispatch::IntegrationTest assert_response :success assert_select 'h2', text: 'Results' assert_select '.result-list[data-result-list-url=?]', - "#{Settings.coreURL.chomp('/')}/iof/3.0/events/#{event.id}/result_list.xml" + "/iof/3.0/events/#{event.id}/result_list.xml" assert_select 'h2', text: 'Course Maps' end diff --git a/test/system/smoke_test.rb b/test/system/smoke_test.rb new file mode 100644 index 0000000..ef0bb90 --- /dev/null +++ b/test/system/smoke_test.rb @@ -0,0 +1,13 @@ +require 'application_system_test_case' + +class SmokeTest < ApplicationSystemTestCase + test "club home page renders without console errors" do + visit_club '/' + assert_selector 'header h1', text: 'Club A Orienteering' + end + + test "apex home page renders" do + visit_apex '/' + assert_selector 'body' + end +end From 2b1530e8cdb4d74afe00a01dc3e927b53c9d8b95 Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Fri, 31 Jul 2026 09:04:37 +0200 Subject: [PATCH 11/16] Add browser system tests for the critical club site flows Covers the P0 flows from the browser test plan in a real headless browser: the SSO round trip, the Knockout event and results editors, course registration including register-others, in-place jEditable editing, map editing with image upload, the calendar, the event lists, and club settings/privileges admin. 26 tests, each also failing on unexpected browser console errors. Bugs the tests exposed, fixed here: - The Register Others panel was dropped during the port; restored on the event page while registration is open - Pages with a rich-text area crashed all page JS when the deploy-time Redactor library is absent; degrades to a plain textarea now - The person picker dropped or resurfaced stale autocomplete responses arriving out of order (also broken in the legacy app) - The event list's showing-events-after/before headers never rendered due to an inverted null check (also broken in the legacy app) - Fixture events silently had no classification (explicit-id fixtures referenced by label) - The SSO endpoints now tolerate a port in the return host (as any non-standard-port deployment sends), with a strict host format check --- .../javascripts/clubsite/wjr/event-list.js | 4 +- app/assets/javascripts/clubsite/wjr/forms.js | 6 + .../clubsite/wjr/wysiwyg.redactor.js | 5 + app/controllers/sso_controller.rb | 25 +++- .../events/_course_registration.html.erb | 24 ++++ test/application_system_test_case.rb | 42 +++++- test/fixtures/events.yml | 8 +- test/system/calendar_test.rb | 30 +++++ test/system/club_admin_test.rb | 51 ++++++++ test/system/course_registration_test.rb | 107 +++++++++++++++ test/system/event_editor_test.rb | 123 ++++++++++++++++++ test/system/event_listing_test.rb | 37 ++++++ test/system/in_place_editing_test.rb | 57 ++++++++ test/system/map_editor_test.rb | 62 +++++++++ test/system/results_editor_test.rb | 64 +++++++++ test/system/sso_round_trip_test.rb | 45 +++++++ 16 files changed, 675 insertions(+), 15 deletions(-) create mode 100644 test/system/calendar_test.rb create mode 100644 test/system/club_admin_test.rb create mode 100644 test/system/course_registration_test.rb create mode 100644 test/system/event_editor_test.rb create mode 100644 test/system/event_listing_test.rb create mode 100644 test/system/in_place_editing_test.rb create mode 100644 test/system/map_editor_test.rb create mode 100644 test/system/results_editor_test.rb create mode 100644 test/system/sso_round_trip_test.rb diff --git a/app/assets/javascripts/clubsite/wjr/event-list.js b/app/assets/javascripts/clubsite/wjr/event-list.js index 3ae472c..3dbbaaf 100644 --- a/app/assets/javascripts/clubsite/wjr/event-list.js +++ b/app/assets/javascripts/clubsite/wjr/event-list.js @@ -87,12 +87,12 @@ WJR['event-list'] = (function ($, moment, ko, club) { }; viewModel.formattedStartTime = ko.computed(function () { - var hasStartTime = (this.startTime() === null); + var hasStartTime = (this.startTime() !== null && this.startTime() !== undefined); return hasStartTime ? this.startTime().format('MMMM Do YYYY') : null; }, viewModel); viewModel.formattedEndTime = ko.computed(function () { - var hasEndTime = (this.endTime() === null); + var hasEndTime = (this.endTime() !== null && this.endTime() !== undefined); return hasEndTime ? this.endTime().format('MMMM Do YYYY') : null; }, viewModel); diff --git a/app/assets/javascripts/clubsite/wjr/forms.js b/app/assets/javascripts/clubsite/wjr/forms.js index 7ded94d..df39194 100644 --- a/app/assets/javascripts/clubsite/wjr/forms.js +++ b/app/assets/javascripts/clubsite/wjr/forms.js @@ -52,6 +52,12 @@ WJR.forms = (function ($, _) { $.ajax({ url: "/users/index.json?term=" + query + "&allowFake=" + (options.allowFake ? 'true' : 'false'), success: function (data) { + // Responses can arrive out of order while the user types; a + // stale response would hide the menu again. Only the response + // for what is in the input now may be shown. + if ($(element).val() !== query) { + return; + } if (options.createNew) { data.push({ position: "bottom", diff --git a/app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js b/app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js index f5c0832..5670007 100644 --- a/app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js +++ b/app/assets/javascripts/clubsite/wjr/wysiwyg.redactor.js @@ -8,6 +8,11 @@ WJR.wysiwyg = (function ($) { var wysiwyg = {}; wysiwyg.updateTextareas = function () { return; }; wysiwyg.createRichTextArea = function (element) { + // Degrade to a plain textarea when the redactor library isn't mounted; + // without this guard every page with a rich-text area throws on load. + if ($.fn.redactor === undefined) { + return; + } $(element).redactor({ toolbarFixed: true, toolbarFixedBox: true, diff --git a/app/controllers/sso_controller.rb b/app/controllers/sso_controller.rb index 578a85f..8ab44f8 100644 --- a/app/controllers/sso_controller.rb +++ b/app/controllers/sso_controller.rb @@ -5,32 +5,34 @@ class SsoController < ApplicationController # Sends the visitor back to the club domain with a sign-in token, going # through the apex sign-in form first when needed. def authorize - club = Club.find_by(domain: params[:return_host].to_s) + return_host = params[:return_host].to_s + club = find_club(return_host) if club.nil? redirect_to root_path, alert: 'Unknown club website.' return end unless user_signed_in? - session[:sso_return_host] = club.domain + session[:sso_return_host] = return_host session[:sso_return_path] = safe_return_path redirect_to new_user_session_path return end - token = SsoToken.generate(current_user, club.domain) + token = SsoToken.generate(current_user, return_host) query = { token: token, path: safe_return_path }.compact.to_query - redirect_to "#{club.domain_protocol}://#{club.domain}/sso/consume?#{query}", + redirect_to "#{club.domain_protocol}://#{return_host}/sso/consume?#{query}", allow_other_host: true end # Ends the apex session as part of signing out of a club website def logout - club = Club.find_by(domain: params[:return_host].to_s) + return_host = params[:return_host].to_s + club = find_club(return_host) sign_out(current_user) if user_signed_in? if club - redirect_to "#{club.domain_protocol}://#{club.domain}/users/logoutComplete", + redirect_to "#{club.domain_protocol}://#{return_host}/users/logoutComplete", allow_other_host: true else redirect_to root_path @@ -39,6 +41,17 @@ def logout private + # Clubs are looked up by domain, tolerating a port in the return host: the + # club domains carry no port in production, but the visitor's Host header + # does whenever the server runs on a non-standard port (as the system-test + # server does). The strict format check keeps anything but a plain + # host[:port] out of the redirect target. + def find_club(return_host) + return nil unless return_host.match?(/\A[a-z0-9.-]+(:\d+)?\z/i) + + Club.find_by(domain: return_host) || Club.find_by(domain: return_host.sub(/:\d+\z/, '')) + end + # Only relative paths may be forwarded to the club domain def safe_return_path path = params[:return_path].to_s diff --git a/app/views/clubsite/events/_course_registration.html.erb b/app/views/clubsite/events/_course_registration.html.erb index 5f7824d..ab38c69 100644 --- a/app/views/clubsite/events/_course_registration.html.erb +++ b/app/views/clubsite/events/_course_registration.html.erb @@ -34,3 +34,27 @@
    <% end %>
    + +<% if @registration_open %> +
    +

    Register Others

    + <% if !user_signed_in? %> + Sign in / Sign Up + <% else %> +

    To register someone else, choose the course to register them on, then type their name, and pick the person from the drop down list.

    +

    Families: you don't need to register an account for every family member, just type the participating people's names below and an account will automatically be created for each person.

    +
    + +
    +
    + + +
    + + <% end %> +
    +<% end %> diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index f750d82..94b2c72 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -25,6 +25,18 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase Capybara.always_include_port = true + # The clubsite sign-in flow bounces through absolute apex URLs + # (Settings.coreURL), so the apex URL must point at the test server. The + # server port is fixed so the URL can be built before the server boots. + Capybara.server_port = 45_671 + Settings.coreURL = "http://#{Settings.host}:#{Capybara.server_port}" + + setup do + # The browser can only reach the test server over plain http, so the SSO + # redirects onto club domains must not use the fixtures' https protocol. + Club.where(domain_protocol: 'https').update_all(domain_protocol: 'http') + end + # Browser console errors almost always mean broken JS wiring; fail loudly. # Individual tests can allow expected noise via allow_console_errors(/pattern/). # Third-party hosts are unreachable in tests (everything resolves to the @@ -32,7 +44,7 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase IGNORED_CONSOLE_ERRORS = [ %r{/favicon\.ico}, %r{/apple-touch-icon}, %r{/redactor/}, /Google Maps/i, /maps\.googleapis/, /flickr/i, - /facebook/i, /juicer/i, /oss\.maxcdn\.com/ + /facebook/i, /juicer/i, /oss\.maxcdn\.com/, /pinterest/i ].freeze teardown do @@ -54,9 +66,31 @@ def visit_apex(path = '/') def sign_in_through_browser(user, password: 'password123') user.update!(password: password, password_confirmation: password) visit_apex '/users/sign_in' - fill_in 'user[email]', with: user.email - fill_in 'user[password]', with: password - click_button 'Log in' + # The page carries a second user[email] field (forgot password) + within "form[action='/users/sign_in']" do + fill_in 'user[email]', with: user.email + fill_in 'user[password]', with: password + click_button 'Sign in' + end + end + + # Signs in on the apex domain, then follows the SSO handoff so the club + # domain has a signed-in session too. + def sign_in_to_club(user, club: clubs(:cluba)) + sign_in_through_browser(user) + visit_club '/users/login', club: club + assert_selector 'a', text: 'Sign out' + assert_equal club.domain, URI.parse(current_url).host + end + + # Waits for a database-side effect of an in-page AJAX call. Prefer Capybara + # matchers whenever the effect is visible in the DOM. + def wait_for_condition(timeout: Capybara.default_max_wait_time, message: 'condition not met in time') + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout + until yield + flunk message if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline + sleep 0.1 + end end private diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml index 5ee5070..d9f2c67 100644 --- a/test/fixtures/events.yml +++ b/test/fixtures/events.yml @@ -1,7 +1,9 @@ cluba_race: name: Spring Sprint club: cluba - event_classification: local + # The classification fixtures carry explicit production ids, so label + # references do not resolve; reference the ids directly. + event_classification_id: 4 # local series: cluba_wednesday date: 2025-06-15 17:00:00 finish_date: 2025-06-15 19:00:00 @@ -11,12 +13,12 @@ cluba_race: cluba_training: name: Evening Training club: cluba - event_classification: club + event_classification_id: 5 # club date: 2025-07-02 01:30:00 cluba_external: name: External Championships club: cluba - event_classification: regional + event_classification_id: 3 # regional date: 2025-08-10 16:00:00 custom_url: https://example.com/external-event diff --git a/test/system/calendar_test.rb b/test/system/calendar_test.rb new file mode 100644 index 0000000..9e92c51 --- /dev/null +++ b/test/system/calendar_test.rb @@ -0,0 +1,30 @@ +require 'application_system_test_case' + +class CalendarTest < ApplicationSystemTestCase + test "the calendar renders the club's events and navigates by month" do + # Put a fixture event in the middle of the current month so it is on the + # initially displayed calendar regardless of timezone edges. + now = Time.now.utc + events(:cluba_race).update!(date: Time.utc(now.year, now.month, 15, 18), finish_date: nil) + + visit_club '/events/index' + + # FullCalendar builds the grid and fetches /club/:id/events.json + assert_selector '.fc-event', text: 'Spring Sprint' + assert_selector '.series-legend li', text: 'Wednesday Evening Series' + + # Month navigation pushes the day-first date of the new view onto the URL + calendar = find('.wjr-calendar') + displayed_month = Date.new(calendar['data-calendar-year'].to_i, + calendar['data-calendar-month'].to_i + 1, 1) + next_month = displayed_month >> 1 + + find('.fc-button-next').click + assert_no_selector '.fc-event', text: 'Spring Sprint' + assert_current_path format('/events/index/01-%02d-%d', next_month.month, next_month.year) + + find('.fc-button-prev').click + assert_selector '.fc-event', text: 'Spring Sprint' + assert_current_path format('/events/index/01-%02d-%d', displayed_month.month, displayed_month.year) + end +end diff --git a/test/system/club_admin_test.rb b/test/system/club_admin_test.rb new file mode 100644 index 0000000..aa5d053 --- /dev/null +++ b/test/system/club_admin_test.rb @@ -0,0 +1,51 @@ +require 'application_system_test_case' + +class ClubAdminTest < ApplicationSystemTestCase + test "editing the club settings persists" do + sign_in_to_club users(:webmaster) + visit_club '/clubs/edit' + assert_selector 'h1', text: 'Edit Club Information' + + fill_in 'club[name]', with: 'Club A Orienteering Society' + fill_in 'club[location]', with: 'Vancouver, BC' + fill_in 'club[url]', with: 'https://cluba.example.com' + select 'America/Whitehorse', from: 'club[timezone]' + click_button 'Save' + + assert_text 'Updated club' + assert_selector 'h1', text: 'Admin tools' + # The new name shows up in the club layout header + assert_selector 'header h1', text: 'Club A Orienteering Society' + + club = clubs(:cluba).reload + assert_equal 'Club A Orienteering Society', club.name + assert_equal 'Vancouver, BC', club.location + assert_equal 'https://cluba.example.com', club.url + assert_equal 'America/Whitehorse', club.timezone + end + + test "granting and revoking a privilege" do + sign_in_to_club users(:webmaster) + visit_club '/privileges/' + assert_selector 'h1', text: 'Privileges' + + # The webmaster can only manage groups at or below their own level + assert_selector 'dt', text: 'Webmaster' + assert_selector 'dt', text: 'Executive' + assert_no_selector 'dt', text: 'Administrator' + + select 'Mary Member', from: 'privilege[user_id]' + select 'Executive', from: 'privilege[group_id]' + click_button 'Add' + + assert_text 'The privilege has been added.' + row = find('tr', text: 'Mary Member') + row.assert_text 'Executive' + assert Privilege.exists?(user: users(:member), user_group: groups(:cluba_executive)) + + find('tr', text: 'Mary Member').find('button').click + assert_text 'The privilege has been deleted.' + assert_no_selector 'tr', text: 'Mary Member' + assert_not Privilege.exists?(user: users(:member), user_group: groups(:cluba_executive)) + end +end diff --git a/test/system/course_registration_test.rb b/test/system/course_registration_test.rb new file mode 100644 index 0000000..3d44982 --- /dev/null +++ b/test/system/course_registration_test.rb @@ -0,0 +1,107 @@ +require 'application_system_test_case' + +class CourseRegistrationTest < ApplicationSystemTestCase + setup do + # Make the fixture event upcoming so registration is open + @event = events(:cluba_race) + @event.update!(date: 3.days.from_now, finish_date: nil) + end + + test "a signed-in user can register and unregister themselves" do + sign_in_to_club users(:executive) + visit_club "/events/view/#{@event.id}" + assert_selector 'h2', text: 'Course Registration' + + long_course = find('.course', text: 'Long Course') + within long_course do + click_button 'Register' + end + + assert_text 'Registration successful!' + long_course = find('.course', text: 'Long Course') + within long_course do + assert_text 'Evan Executive' + assert_button 'Unregister' + assert_no_button 'Register', exact_text: true + end + result = Result.find_by(course_id: courses(:race_long).id, user_id: users(:executive).id) + assert_equal users(:executive).id, result.registrant_id + + within find('.course', text: 'Long Course') do + click_button 'Unregister' + end + assert_text 'Unregistration successful!' + within find('.course', text: 'Long Course') do + assert_no_text 'Evan Executive' + assert_button 'Register', exact_text: true + end + assert_not Result.exists?(course_id: courses(:race_long).id, user_id: users(:executive).id) + end + + test "registering an existing person through the register-others picker" do + sign_in_to_club users(:executive) + visit_club "/events/view/#{@event.id}" + + within '.register-others' do + select 'Score Course', from: 'RegisterOthersCourse' + end + fill_in 'RegisterOthersUserName', with: 'Mary' + find('ul.typeahead li a', text: 'Mary Member').click + find('#RegisterOthersSubmit').click + + assert_text 'Registration successful!' + within find('.course', text: 'Score Course') do + assert_text 'Mary Member' + end + result = Result.find_by(course_id: courses(:race_score).id, user_id: users(:member).id) + assert_equal users(:executive).id, result.registrant_id + end + + test "registering a brand-new person creates their account" do + sign_in_to_club users(:executive) + visit_club "/events/view/#{@event.id}" + + within '.register-others' do + select 'Score Course', from: 'RegisterOthersCourse' + end + fill_in 'RegisterOthersUserName', with: 'Robin Racer' + accept_confirm do + find('#RegisterOthersSubmit').click + end + + assert_text 'Registration successful!' + within find('.course', text: 'Score Course') do + assert_text 'Robin Racer' + end + new_user = User.find_by(name: 'Robin Racer') + assert_not_nil new_user + assert_nil new_user.email + result = Result.find_by(course_id: courses(:race_score).id, user_id: new_user.id) + assert_equal users(:executive).id, result.registrant_id + end + + test "signed-out visitors are sent to the sign-in flow" do + visit_club "/events/view/#{@event.id}" + + within '.register-others' do + assert_link 'Sign in / Sign Up' + end + + within find('.course', text: 'Long Course') do + click_button 'Register' + end + # Registration requires sign-in; the visitor lands on the apex form + assert_selector 'h3', text: 'Sign in' + assert_equal Settings.host, URI.parse(current_url).host + end + + test "closed registration hides the registration controls" do + @event.update!(registration_deadline: 1.day.ago) + sign_in_to_club users(:executive) + visit_club "/events/view/#{@event.id}" + + assert_text 'Registration is Closed' + assert_no_button 'Register', exact_text: true + assert_no_selector '.register-others' + end +end diff --git a/test/system/event_editor_test.rb b/test/system/event_editor_test.rb new file mode 100644 index 0000000..aa438e8 --- /dev/null +++ b/test/system/event_editor_test.rb @@ -0,0 +1,123 @@ +require 'application_system_test_case' + +class EventEditorTest < ApplicationSystemTestCase + test "creating an event with courses and an organizer through the editors" do + sign_in_to_club users(:executive) + visit_club '/events/edit' + assert_selector 'h1', text: 'Add Event' + + fill_in 'EventName', with: 'Browser Test Meet' + # The date picker fills in today's date on focus, so select-all and + # overtype instead of a plain fill_in (which the maxlength would block) + find('#EventDate').send_keys [:control, 'a'], '2026-09-10' + fill_in 'EventTime', with: '18:30' + find('#EventEventClassificationId').find(:option, text: /\ALocal/).select_option + select 'Wednesday Evening Series', from: 'EventSeriesId' + select 'Forest Map', from: 'EventMapId' + fill_in 'EventDescription', with: 'A fun evening race.' + + # The organizer picker autocompletes from /users/index.json + fill_in 'organizers', with: 'Oscar' + find('ul.typeahead li a', text: 'Oscar Organizer').click + within '.edit-event-organizers' do + assert_selector 'td', text: 'Oscar Organizer' + # The role select is populated from /roles/index.json + find('tbody tr select').select('Course Planner') + end + + # Two courses through the Knockout course editor + within '.edit-event-courses' do + click_button 'Course' + click_button 'Course' + rows = all('tbody tr') + within rows[0] do + inputs = all('input') + inputs[0].set('Long') + inputs[1].set('5200') + inputs[2].set('180') + inputs[3].set('The long loop') + end + within rows[1] do + all('input')[0].set('Score') + find('select').select('Score-O Points') + end + end + + click_button 'Save' + + assert_text 'The event has been updated.' + assert_selector 'h1', text: 'Browser Test Meet' + assert_text 'A fun evening race.' + assert_selector 'h3', text: 'Long' + assert_selector 'h3', text: 'Score' + + event = Event.find_by(name: 'Browser Test Meet') + assert_not_nil event + # 18:30 in the club's timezone (America/Vancouver) + assert_equal Time.utc(2026, 9, 11, 1, 30), event.date + assert_equal maps(:cluba_forest).id, event.map_id + assert_equal series(:cluba_wednesday).id, event.series_id + assert_equal 4, event.event_classification_id + + assert_equal %w[Long Score], event.courses.map(&:name).sort + long_course = event.courses.find_by(name: 'Long') + assert_equal 5200, long_course.distance + assert_equal 180, long_course.climb + assert_equal 'The long loop', long_course.description + assert_not long_course.is_score_o + assert event.courses.find_by(name: 'Score').is_score_o + + assert_equal 1, event.organizers.count + organizer = event.organizers.first + assert_equal users(:organizer).id, organizer.user_id + assert_equal roles(:course_planner).id, organizer.role_id + end + + test "editing an event rehydrates the editors and persists changes" do + event = events(:cluba_race) + score_course_id = courses(:race_score).id + sign_in_to_club users(:executive) + visit_club "/events/edit/#{event.id}" + assert_selector 'h1', text: 'Edit Event' + + # The organizer editor rehydrates from the JSON blob, with the stored role + within '.edit-event-organizers' do + assert_selector 'td', text: 'Oscar Organizer' + selected_role = find('tbody tr select').all('option').detect(&:selected?) + assert_equal 'Organizer', selected_role.text + end + + within '.edit-event-courses' do + assert_selector 'tbody tr', count: 2 + names = all('tbody tr td:first-child input').map(&:value) + assert_equal ['Long Course', 'Score Course'], names.sort + + # Update the long course in place + long_row = all('tbody tr').detect { |row| row.first('input').value == 'Long Course' } + within long_row do + inputs = all('input') + inputs[0].set('Longer Course') + inputs[1].set('6000') + end + + # Delete the score course; this posts to /courses/delete immediately + score_row = all('tbody tr').detect { |row| row.first('input').value == 'Score Course' } + accept_confirm do + score_row.find('button').click + end + assert_selector 'tbody tr', count: 1 + end + wait_for_condition(message: 'course was not deleted') { !Course.exists?(score_course_id) } + + within '.edit-event-organizers' do + find('tbody tr select').select('Course Planner') + end + + click_button 'Save' + assert_text 'The event has been updated.' + + assert_equal ['Longer Course'], event.reload.courses.map(&:name) + assert_equal 6000, courses(:race_long).reload.distance + assert_equal [roles(:course_planner).id], event.organizers.map(&:role_id) + end +end diff --git a/test/system/event_listing_test.rb b/test/system/event_listing_test.rb new file mode 100644 index 0000000..d0eb593 --- /dev/null +++ b/test/system/event_listing_test.rb @@ -0,0 +1,37 @@ +require 'application_system_test_case' + +class EventListingTest < ApplicationSystemTestCase + test "the event listing populates from the IOF XML feed" do + events(:cluba_training).update!(date: 1.week.from_now) + events(:cluba_race).update!(date: 3.weeks.from_now, finish_date: nil) + + visit_club '/events/listing' + + assert_selector '.event-box', text: 'Evening Training' + assert_selector '.event-box', text: 'Spring Sprint' + within find('.event-box', text: 'Evening Training') do + assert_selector '.event-box-classification', text: 'Club' + end + assert_text(/Showing events after/) + end + + test "the Older button extends the window backwards" do + Event.create!(name: 'Autumn Classic', club: clubs(:cluba), + event_classification: event_classifications(:local), + date: 3.months.ago) + + visit_club '/events/listing' + assert_selector '.event-list' + assert_no_selector '.event-box', text: 'Autumn Classic' + + click_button 'Older' + assert_selector '.event-box', text: 'Autumn Classic' + end + + test "the home page event list populates from the feed" do + events(:cluba_training).update!(date: 1.week.from_now) + + visit_club '/' + assert_selector '.event-box', text: 'Evening Training' + end +end diff --git a/test/system/in_place_editing_test.rb b/test/system/in_place_editing_test.rb new file mode 100644 index 0000000..15d28c0 --- /dev/null +++ b/test/system/in_place_editing_test.rb @@ -0,0 +1,57 @@ +require 'application_system_test_case' + +class InPlaceEditingTest < ApplicationSystemTestCase + test "editing a home page content block in place" do + block = content_blocks(:cluba_general_information) + sign_in_to_club users(:executive) + + find("#content-block-#{block.id}").click + within "#content-block-#{block.id}" do + find('textarea').set('

    Hello from the club

    ') + click_button 'Save' + end + + assert_selector "#content-block-#{block.id} h2", text: 'Hello from the club' + assert_equal '

    Hello from the club

    ', block.reload.content + + # The saved content survives a reload + visit_club '/' + assert_selector "#content-block-#{block.id} h2", text: 'Hello from the club' + end + + test "editing a resource page title and body in place" do + page_record = pages(:cluba_trail_guide) + sign_in_to_club users(:executive) + visit_club "/pages/#{page_record.id}" + + find("#page-resource-title-#{page_record.id}").click + within "#page-resource-title-#{page_record.id}" do + find('input').set('Trail Guide 2026') + click_button 'Save' + end + assert_selector 'h1', text: 'Trail Guide 2026' + assert_equal 'Trail Guide 2026', page_record.reload.name + + find("#page-resource-#{page_record.id}").click + within "#page-resource-#{page_record.id}" do + find('textarea').set('

    All the trails, now with directions.

    ') + click_button 'Save' + end + assert_selector "#page-resource-#{page_record.id} p", text: 'All the trails, now with directions.' + assert_equal '

    All the trails, now with directions.

    ', page_record.reload.content + + visit_club "/pages/#{page_record.id}" + assert_selector 'h1', text: 'Trail Guide 2026' + assert_selector "#page-resource-#{page_record.id} p", text: 'All the trails, now with directions.' + end + + test "members without the edit privilege get no in-place editors" do + block = content_blocks(:cluba_general_information) + sign_in_to_club users(:member) + + assert_selector "#content-block-#{block.id}" + assert_no_selector "#content-block-#{block.id}.wjr-editable" + find("#content-block-#{block.id}").click + assert_no_selector "#content-block-#{block.id} textarea" + end +end diff --git a/test/system/map_editor_test.rb b/test/system/map_editor_test.rb new file mode 100644 index 0000000..083492e --- /dev/null +++ b/test/system/map_editor_test.rb @@ -0,0 +1,62 @@ +require 'application_system_test_case' + +class MapEditorTest < ApplicationSystemTestCase + teardown do + # Remove any files written to the data volume during the test + FileUtils.rm_rf(File.join(Settings.dataFolder, clubs(:cluba).id.to_s)) + end + + test "creating a map with an image upload renders the thumbnails" do + sign_in_to_club users(:executive) + visit_club '/maps/edit' + assert_selector 'h1', text: 'Add Map' + + fill_in 'map[name]', with: 'Browser Test Map' + fill_in 'map[scale]', with: '12500' + attach_file 'map[image]', Rails.root.join('test/fixtures/files/logo.png') + click_button 'Save' + + assert_text 'The map has been updated.' + map = Map.find_by(name: 'Browser Test Map') + assert_not_nil map + assert_equal 12_500, map.scale + assert MediaStore.new(clubs(:cluba).id, 'Map').exists?(map.id) + + # The map page shows the uploaded rendering (a 404 here would also trip + # the console-error teardown) + assert_selector 'h1', text: 'Browser Test Map' + assert_selector "img[src*='/maps/rendering/#{map.id}']" + end + + test "editing a map persists changes and shows the existing image" do + map = maps(:cluba_forest) + upload = Rack::Test::UploadedFile.new(Rails.root.join('test/fixtures/files/logo.png'), 'image/png') + MediaStore.new(clubs(:cluba).id, 'Map').store(map.id, upload) + + sign_in_to_club users(:executive) + visit_club "/maps/edit/#{map.id}" + assert_selector 'h1', text: 'Edit Map' + assert_selector "img[src*='/maps/rendering/#{map.id}']" + + fill_in 'map[name]', with: 'Forest Map West' + click_button 'Save' + + assert_text 'The map has been updated.' + assert_selector 'h1', text: 'Forest Map West' + assert_equal 'Forest Map West', map.reload.name + end + + test "the draggable marker update endpoint persists the new location" do + map = maps(:cluba_forest) + sign_in_to_club users(:executive) + visit_club "/maps/edit/#{map.id}" + + # Google Maps never loads in tests, so simulate the marker drop by posting + # what map.js posts, from the page context (exercises the CSRF wiring). + page.execute_script("jQuery.post('/maps/update/#{map.id}/49.5/-123.45')") + wait_for_condition(message: 'map location was not updated') do + map.reload.lat.to_f.round(2) == 49.5 + end + assert_in_delta(-123.45, map.lng.to_f) + end +end diff --git a/test/system/results_editor_test.rb b/test/system/results_editor_test.rb new file mode 100644 index 0000000..32f435d --- /dev/null +++ b/test/system/results_editor_test.rb @@ -0,0 +1,64 @@ +require 'application_system_test_case' + +class ResultsEditorTest < ApplicationSystemTestCase + test "entering results and adding a new person persists on save" do + event = events(:cluba_race) + sign_in_to_club users(:executive) + visit_club "/events/editResults/#{event.id}" + + # The editor loads its data from the event JSON + assert_selector 'h2', text: 'Long Course' + assert_selector 'h2', text: 'Score Course' + assert_selector 'td', text: 'Mary Member' + + # Enter a time and status for the existing registrant + within find('tr', text: 'Mary Member') do + segments = all('input.time-segment') + segments[0].set('01') + segments[1].set('05') + segments[2].set('30') + find('select').select('DSQ') + end + + # Add a brand-new person (the fake-user path) to the long course + find('select[data-bind*="selectedCourse"]').select('Long Course') + fill_in 'competitorResults', with: 'Nellie Newbie' + find('ul.typeahead li a', text: 'Create New User (Nellie Newbie)').click + assert_selector 'td', text: 'Nellie Newbie' + + click_button 'Save' + + # Saving posts the JSON blob and redirects to the event page + assert_selector 'h1', text: 'Spring Sprint' + result = results(:member_long).reload + assert_in_delta 3930.0, result.time_seconds + assert_equal :disqualified, result.status + assert events(:cluba_race).reload.results_posted + + new_user = User.find_by(name: 'Nellie Newbie') + assert_not_nil new_user + assert_nil new_user.email + new_result = Result.find_by(user_id: new_user.id, course_id: courses(:race_long).id) + assert_not_nil new_result + assert_equal :ok, new_result.status + end + + test "deleting a result removes it immediately over AJAX" do + event = events(:cluba_race) + result = results(:organizer_long) + sign_in_to_club users(:executive) + visit_club "/events/editResults/#{event.id}" + + assert_selector 'td', text: 'Oscar Organizer' + accept_confirm do + find('tr', text: 'Oscar Organizer').find('button').click + end + assert_no_selector 'td', text: 'Oscar Organizer' + wait_for_condition(message: 'result was not deleted') { !Result.exists?(result.id) } + + # The deletion survives a reload + visit_club "/events/editResults/#{event.id}" + assert_selector 'td', text: 'Mary Member' + assert_no_selector 'td', text: 'Oscar Organizer' + end +end diff --git a/test/system/sso_round_trip_test.rb b/test/system/sso_round_trip_test.rb new file mode 100644 index 0000000..470afd2 --- /dev/null +++ b/test/system/sso_round_trip_test.rb @@ -0,0 +1,45 @@ +require 'application_system_test_case' + +class SsoRoundTripTest < ApplicationSystemTestCase + test "the club sign-in link goes through the apex form and returns signed in" do + user = users(:member) + user.update!(password: 'password123', password_confirmation: 'password123') + + visit_club '/' + click_link 'Sign in/Sign up' + + # Sent to the sign-in form on the apex domain + assert_selector 'h3', text: 'Sign in' + assert_equal Settings.host, URI.parse(current_url).host + + within "form[action='/users/sign_in']" do + fill_in 'user[email]', with: user.email + fill_in 'user[password]', with: 'password123' + click_button 'Sign in' + end + + # ... and handed back to the club domain with a signed-in session + assert_selector 'a', text: 'Sign out' + assert_equal clubs(:cluba).domain, URI.parse(current_url).host + assert_selector 'header h1', text: 'Club A Orienteering' + end + + test "an existing apex session carries over to the club domain" do + sign_in_to_club users(:member) + assert_no_selector 'a', text: 'Sign in/Sign up' + end + + test "signing out on the club domain ends both sessions" do + sign_in_to_club users(:member) + + click_link 'Sign out' + assert_text 'You have been signed out.' + assert_equal clubs(:cluba).domain, URI.parse(current_url).host + assert_selector 'a', text: 'Sign in/Sign up' + + # The apex session is gone too + visit_apex '/' + assert_link 'Sign up' + assert_no_selector 'a', text: 'Sign out' + end +end From 4378390ceadc4be431c7704630f461156a7b7ad5 Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Fri, 31 Jul 2026 09:27:01 +0200 Subject: [PATCH 12/16] Add browser system tests for the remaining club site flows Completes the automatable scope of the browser test plan: live results toggle and feed rendering, the registrant comment modal, printable entries, series admin with the color picker, branding uploads and their layout effects, user merge and the duplicates screen, .embed iframe variants, the remaining admin CRUD through real forms, and the password reset mail flow. 42 system tests total. Bugs the tests exposed, fixed here: - A misnamed callback action (update_live vs update_live_result_list) broke every request to the results controller wherever Rails raises on missing callback actions, including the live results feed, and left live result uploads without their authentication gate - The live results header bound a nonexistent creationDate property, so the results-produced-on date never rendered --- app/controllers/results_controller.rb | 2 +- .../events/_knockout_result_list.html.erb | 2 +- test/system/admin_crud_test.rb | 111 ++++++++++++++++++ test/system/branding_test.rb | 95 +++++++++++++++ test/system/embed_pages_test.rb | 29 +++++ test/system/live_results_test.rb | 63 ++++++++++ test/system/password_reset_test.rb | 54 +++++++++ test/system/printable_entries_test.rb | 31 +++++ test/system/registrant_comment_test.rb | 53 +++++++++ test/system/series_admin_test.rb | 60 ++++++++++ test/system/user_merge_test.rb | 34 ++++++ 11 files changed, 532 insertions(+), 2 deletions(-) create mode 100644 test/system/admin_crud_test.rb create mode 100644 test/system/branding_test.rb create mode 100644 test/system/embed_pages_test.rb create mode 100644 test/system/live_results_test.rb create mode 100644 test/system/password_reset_test.rb create mode 100644 test/system/printable_entries_test.rb create mode 100644 test/system/registrant_comment_test.rb create mode 100644 test/system/series_admin_test.rb create mode 100644 test/system/user_merge_test.rb diff --git a/app/controllers/results_controller.rb b/app/controllers/results_controller.rb index c850a9d..e22c1db 100644 --- a/app/controllers/results_controller.rb +++ b/app/controllers/results_controller.rb @@ -1,7 +1,7 @@ require 'nokogiri' class ResultsController < ApplicationController - before_action :authenticate_user!, :only => [:update_live, :process_result_list] + before_action :authenticate_user!, :only => [:update_live_result_list, :process_result_list] # Result list uploads come from external tools authenticating over HTTP Basic, # which cannot supply a CSRF token. skip_forgery_protection only: [:process_result_list, :update_live_result_list, :update_result_list] diff --git a/app/views/clubsite/events/_knockout_result_list.html.erb b/app/views/clubsite/events/_knockout_result_list.html.erb index fc8e146..77278cc 100644 --- a/app/views/clubsite/events/_knockout_result_list.html.erb +++ b/app/views/clubsite/events/_knockout_result_list.html.erb @@ -1,7 +1,7 @@ <%# locals: mode ('normal' or 'live') %>
    <% if mode == 'live' %> -

    Results produced on

    +

    Results produced on

    <% end %>

    diff --git a/test/system/admin_crud_test.rb b/test/system/admin_crud_test.rb new file mode 100644 index 0000000..95bac03 --- /dev/null +++ b/test/system/admin_crud_test.rb @@ -0,0 +1,111 @@ +require 'application_system_test_case' + +class AdminCrudTest < ApplicationSystemTestCase + test "adding, editing and deleting a membership" do + sign_in_to_club users(:executive) + visit_club '/memberships/' + assert_selector 'h1', text: 'Memberships' + + select 'Oscar Organizer', from: 'membership[user_id]' + fill_in 'membership[year]', with: '2026' + click_button 'Add membership' + + assert_text 'The membership has been updated.' + membership = Membership.find_by(user: users(:organizer), club: clubs(:cluba), year: 2026) + assert_not_nil membership + assert_selector 'tr', text: 'Oscar Organizer' + + within find('tr', text: 'Oscar Organizer') do + click_link 'Edit' + end + assert_selector 'h1', text: 'Membership' + fill_in 'membership[year]', with: '2025' + click_button 'Update' + + assert_text 'The membership has been updated.' + assert_equal 2025, membership.reload.year + + within find('tr', text: 'Oscar Organizer') do + click_button 'Remove' + end + assert_text 'The membership has been deleted.' + assert_no_selector 'tr', text: 'Oscar Organizer' + assert_not Membership.exists?(membership.id) + end + + test "adding and deleting an official through the person picker" do + sign_in_to_club users(:webmaster) + visit_club '/officials/' + assert_selector 'h1', text: 'List of officials' + + fill_in 'UserName', with: 'Wendy' + find('ul.typeahead li a', text: 'Wendy Webmaster').click + within "form[action='/officials/add']" do + select 'Level 2', from: 'official[official_classification_id]' + fill_in 'official[date]', with: '2026-05-01' + find('button[type="submit"]').click + end + + assert_text 'The official has been added.' + official = Official.find_by(user: users(:webmaster), + official_classification: official_classifications(:level_two)) + assert_not_nil official + assert_equal Date.new(2026, 5, 1), official.date.to_date + assert_selector 'tr', text: 'Wendy Webmaster' + + within find('tr', text: 'Wendy Webmaster') do + find('button.btn-danger').click + end + assert_text 'The official was deleted.' + assert_no_selector 'tr', text: 'Wendy Webmaster' + assert_not Official.exists?(official.id) + end + + test "editing a map standard" do + map_standard = MapStandard.create!(name: 'ISOM', color: 'rgba(0,0,0,1)') + + sign_in_to_club users(:admin) + visit_club '/mapStandards/' + assert_selector 'h1', text: 'Map Standards' + + within find('tr', text: 'ISOM') do + find("a[href='/mapStandards/edit/#{map_standard.id}']").click + end + assert_selector 'h1', text: 'Map standard' + + fill_in 'map_standard[name]', with: 'ISOM 2017' + fill_in 'map_standard[color]', with: 'rgba(10,20,30,1)' + fill_in 'map_standard[description]', with: 'Forest mapping standard' + click_button 'Save' + + assert_text 'The map standard has been updated.' + assert_selector 'tr', text: 'ISOM 2017' + map_standard.reload + assert_equal 'ISOM 2017', map_standard.name + assert_equal 'rgba(10,20,30,1)', map_standard.color + assert_equal 'Forest mapping standard', map_standard.description + end + + test "editing a role" do + role = roles(:course_planner) + + sign_in_to_club users(:admin) + visit_club '/roles/' + assert_selector 'h1', text: 'Roles' + + within find('tr', text: 'Course Planner') do + find("a[href='/roles/edit/#{role.id}']").click + end + assert_selector 'h1', text: 'Role' + + fill_in 'role[name]', with: 'Course Setter' + fill_in 'role[description]', with: 'Sets the courses' + click_button 'Save' + + assert_text 'The role has been updated.' + assert_selector 'tr', text: 'Course Setter' + role.reload + assert_equal 'Course Setter', role.name + assert_equal 'Sets the courses', role.description + end +end diff --git a/test/system/branding_test.rb b/test/system/branding_test.rb new file mode 100644 index 0000000..0a3dffc --- /dev/null +++ b/test/system/branding_test.rb @@ -0,0 +1,95 @@ +require 'application_system_test_case' + +class BrandingTest < ApplicationSystemTestCase + # Branding files are addressed through Settings.dataURL, which production + # serves from a separate static host. The browser can only reach the test + # server, so the data folder is pointed at a public/ subdirectory that the + # test server's static file middleware serves. + DATA_DIR = 'system-test-data'.freeze + + setup do + @original_data_folder = Settings.dataFolder + @original_data_url = Settings.dataURL + Settings.dataFolder = Rails.public_path.join(DATA_DIR).to_s + Settings.dataURL = "/#{DATA_DIR}/" + end + + teardown do + FileUtils.rm_rf(Rails.public_path.join(DATA_DIR)) + Settings.dataFolder = @original_data_folder + Settings.dataURL = @original_data_url + end + + test "uploading and deleting the header image and custom CSS" do + sign_in_to_club users(:webmaster) + visit_club '/resources/index' + assert_selector 'h1', text: 'Resources' + assert_text 'Not uploaded' + + within find('tr', text: 'Header Image') do + attach_file 'resource[file]', Rails.root.join('test/fixtures/files/logo.png') + fill_in 'resource[caption]', with: 'Club banner' + click_button 'Upload' + end + assert_text 'The resource has been uploaded.' + within find('tr', text: 'Header Image') do + assert_selector "img[src*='headerImage_100.jpg']" + assert_text 'Club banner' + end + + within find('tr', text: 'CSS Style') do + attach_file 'resource[file]', Rails.root.join('test/fixtures/files/style.css') + click_button 'Upload' + end + within find('tr', text: 'CSS Style') do + assert_link 'style.css' + end + + header = Resource.find_by(club: clubs(:cluba), key: 'headerImage') + style = Resource.find_by(club: clubs(:cluba), key: 'style') + assert File.exist?(header.absolute_path) + assert File.exist?(header.absolute_path('1300')) + assert File.exist?(header.absolute_path('2600')) + assert File.exist?(style.absolute_path) + + # The layout swaps the club name heading for the header image with a + # retina srcset (the tripwire would catch a broken image URL) + visit_club '/' + image = find('header img') + assert_includes image[:src], 'headerImage_1300.jpg' + assert_includes image[:srcset], 'headerImage_1300.jpg' + assert_includes image[:srcset], 'headerImage_2600.jpg' + assert_no_selector 'header h1', text: 'Club A Orienteering' + + # The custom CSS link loads after the clubsite bundle so it wins the cascade + hrefs = page.evaluate_script( + "Array.from(document.querySelectorAll('head link[rel=stylesheet]')).map(function (l) { return l.getAttribute('href'); })" + ) + bundle_index = hrefs.index { |href| href.include?('clubsite') } + style_index = hrefs.index { |href| href.include?('style.css') } + assert_not_nil bundle_index + assert_not_nil style_index + assert_operator style_index, :>, bundle_index + + # Deleting removes the records, the files and the layout customizations + visit_club '/resources/index' + within(find('tr', text: 'Header Image')) { click_button 'Delete' } + assert_text 'The resource has been deleted.' + within find('tr', text: 'Header Image') do + assert_text 'Not uploaded' + end + within(find('tr', text: 'CSS Style')) { click_button 'Delete' } + within find('tr', text: 'CSS Style') do + assert_text 'Not uploaded' + end + + assert_not File.exist?(header.absolute_path) + assert_not File.exist?(header.absolute_path('1300')) + assert_not File.exist?(style.absolute_path) + assert_empty Resource.where(club: clubs(:cluba)) + + visit_club '/' + assert_selector 'header h1', text: 'Club A Orienteering' + assert_no_selector "head link[href*='style.css']", visible: :all + end +end diff --git a/test/system/embed_pages_test.rb b/test/system/embed_pages_test.rb new file mode 100644 index 0000000..86b47e1 --- /dev/null +++ b/test/system/embed_pages_test.rb @@ -0,0 +1,29 @@ +require 'application_system_test_case' + +class EmbedPagesTest < ApplicationSystemTestCase + test "the calendar embed renders bare and the calendar initializes" do + now = Time.now.utc + events(:cluba_race).update!(date: Time.utc(now.year, now.month, 15, 18), finish_date: nil) + + visit_club '/events/index.embed' + + # FullCalendar builds the grid and fetches the club's events + assert_selector '.wjr-calendar' + assert_selector '.fc-event', text: 'Spring Sprint' + assert_selector '.series-legend li', text: 'Wednesday Evening Series' + + # Bare embed layout: no club chrome around the content + assert_no_selector 'nav.navbar' + assert_no_selector 'footer' + assert_no_selector '#content' + end + + test "the maps embed renders bare and initializes" do + visit_club '/maps/index.embed' + + assert_selector 'h1', text: 'Maps' + assert_selector '.multi-marker-map' + assert_no_selector 'nav.navbar' + assert_no_selector 'footer' + end +end diff --git a/test/system/live_results_test.rb b/test/system/live_results_test.rb new file mode 100644 index 0000000..7da217c --- /dev/null +++ b/test/system/live_results_test.rb @@ -0,0 +1,63 @@ +require 'application_system_test_case' + +class LiveResultsTest < ApplicationSystemTestCase + LIVE_RESULT_LIST_XML = <<~XML.freeze + + + Spring Sprint + + Long Course + + MemberMary + 1OK + + + + XML + + test "an organizer can show and hide the live results" do + event = events(:cluba_race) + result_list = ResultList.create!(event: event, user: users(:organizer), + status: ResultList::LIVE_STATUS, visible: false, + data: LIVE_RESULT_LIST_XML, upload_time: Time.current) + + sign_in_to_club users(:organizer) + visit_club "/events/view/#{event.id}" + + # Hidden live results show editors only the toggle button + assert_selector 'h2', text: 'Live Results' + assert_no_text 'these are not finalized' + click_button 'Show Live Results' + + # Toggling persists and re-renders the event page with the list visible + assert_button 'Hide Live Results' + assert result_list.reload.visible + + # The list populates from the live IOF XML feed + assert_text 'Live results - these are not finalized!' + assert_selector '.result-list h3', text: 'Long Course' + assert_selector '.result-list td', text: 'Mary Member' + assert_selector '.result-list td', text: '01:05:30' + # The creation time renders in the browser's local time + assert_text(/Results produced on \w+, June 1[56]th 2025/) + + click_button 'Hide Live Results' + assert_button 'Show Live Results' + assert_not result_list.reload.visible + assert_no_text 'these are not finalized' + end + + test "members do not get the toggle and hidden live results stay hidden" do + event = events(:cluba_race) + ResultList.create!(event: event, user: users(:organizer), + status: ResultList::LIVE_STATUS, visible: false, + data: LIVE_RESULT_LIST_XML, upload_time: Time.current) + + sign_in_to_club users(:member) + visit_club "/events/view/#{event.id}" + + assert_selector 'h1', text: 'Spring Sprint' + assert_no_button 'Show Live Results' + assert_no_text 'these are not finalized' + end +end diff --git a/test/system/password_reset_test.rb b/test/system/password_reset_test.rb new file mode 100644 index 0000000..ff321a4 --- /dev/null +++ b/test/system/password_reset_test.rb @@ -0,0 +1,54 @@ +require 'application_system_test_case' + +class PasswordResetTest < ApplicationSystemTestCase + setup do + # The mail initializer configures SMTP delivery for every environment; + # capture deliveries in memory for the duration of this test instead. + @original_delivery_method = ActionMailer::Base.delivery_method + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.deliveries.clear + end + + teardown do + ActionMailer::Base.delivery_method = @original_delivery_method + end + + test "resetting a password through the emailed link" do + user = users(:member) + user.update!(password: 'old-password-1', password_confirmation: 'old-password-1') + + visit_apex '/users/sign_in' + within "form[action='/users/password']" do + fill_in 'user[email]', with: user.email + click_button 'Reset password' + end + assert_text 'you will receive a password recovery link' + + mail = ActionMailer::Base.deliveries.last + assert_not_nil mail, 'no reset mail was sent' + assert_equal [user.email], mail.to + + link = mail.body.to_s[%r{http://[^"]+reset_password_token=[^"]+}] + assert_not_nil link, 'reset link missing from the mail body' + + visit link + assert_selector 'h2', text: 'Change your password' + fill_in 'user[password]', with: 'brand-new-pass-1' + fill_in 'user[password_confirmation]', with: 'brand-new-pass-1' + click_button 'Change my password' + + # Devise signs the user in after a successful reset + assert_text 'Your password was changed successfully. You are now signed in.' + click_link 'Sign out' + assert_selector 'a', text: 'Sign in' + + # The new password works through the normal sign-in form + visit_apex '/users/sign_in' + within "form[action='/users/sign_in']" do + fill_in 'user[email]', with: user.email + fill_in 'user[password]', with: 'brand-new-pass-1' + click_button 'Sign in' + end + assert_selector 'a', text: 'Sign out' + end +end diff --git a/test/system/printable_entries_test.rb b/test/system/printable_entries_test.rb new file mode 100644 index 0000000..825a0aa --- /dev/null +++ b/test/system/printable_entries_test.rb @@ -0,0 +1,31 @@ +require 'application_system_test_case' + +class PrintableEntriesTest < ApplicationSystemTestCase + test "the printable entries page lists registrations with member checkmarks" do + event = events(:cluba_race) + results(:member_long).update!(registrant_comment: 'Needs a ride') + + sign_in_to_club users(:executive) + visit_club "/events/printableEntries/#{event.id}" + + # The printable layout renders bare, without the club chrome + assert_selector 'h2', text: 'Spring Sprint' + assert_no_selector 'nav.navbar' + assert_no_selector 'footer' + + assert_selector 'h3', text: 'Course: Long Course (2 participants)' + assert_selector 'h3', text: 'Course: Score Course (0 participants)' + + # Mary holds a current-year membership (fixture); Oscar does not + mary = find('tr', text: 'Mary Member') + mary.assert_text '✓' + mary.assert_text 'Needs a ride' + oscar = find('tr', text: 'Oscar Organizer') + oscar.assert_no_text '✓' + + # Each course table carries blank rows for walk-up entries at the desk + long_course_table = mary.ancestor('table') + expected_rows = 1 + 2 + Clubsite::EventsController::NUM_BLANK_ENTRIES + assert_equal expected_rows, long_course_table.all('tr').size + end +end diff --git a/test/system/registrant_comment_test.rb b/test/system/registrant_comment_test.rb new file mode 100644 index 0000000..cb80fba --- /dev/null +++ b/test/system/registrant_comment_test.rb @@ -0,0 +1,53 @@ +require 'application_system_test_case' + +class RegistrantCommentTest < ApplicationSystemTestCase + test "a registrant can add a comment through the modal" do + event = events(:cluba_race) + event.update!(date: 2.weeks.from_now, finish_date: nil) + result = results(:member_long) + + sign_in_to_club users(:member) + visit_club "/events/view/#{event.id}" + + within find('tr', text: 'Mary Member') do + click_button 'Add Comment' + end + within "#change-comment-modal-#{result.id}" do + find('textarea').set('Can offer a ride from downtown') + click_button 'Save' + end + + # Saving posts the comment and returns to the event page + assert_selector 'h1', text: 'Spring Sprint' + assert_equal 'Can offer a ride from downtown', result.reload.registrant_comment + + # The entry now offers editing and shows the comment bubble + within find('tr', text: 'Mary Member') do + assert_button 'Edit Comment' + assert_selector 'button .glyphicon-comment' + end + end + + test "a registrant can change an existing comment" do + event = events(:cluba_race) + event.update!(date: 2.weeks.from_now, finish_date: nil) + result = results(:member_long) + result.update!(registrant_comment: 'Old comment') + + sign_in_to_club users(:member) + visit_club "/events/view/#{event.id}" + + within find('tr', text: 'Mary Member') do + click_button 'Edit Comment' + end + within "#change-comment-modal-#{result.id}" do + assert_selector 'h4', text: 'Change Comment' + assert_equal 'Old comment', find('textarea').value + find('textarea').set('Ride offer withdrawn') + click_button 'Save' + end + + assert_selector 'h1', text: 'Spring Sprint' + assert_equal 'Ride offer withdrawn', result.reload.registrant_comment + end +end diff --git a/test/system/series_admin_test.rb b/test/system/series_admin_test.rb new file mode 100644 index 0000000..e55a035 --- /dev/null +++ b/test/system/series_admin_test.rb @@ -0,0 +1,60 @@ +require 'application_system_test_case' + +class SeriesAdminTest < ApplicationSystemTestCase + test "creating a series with the color picker shows its color on club pages" do + sign_in_to_club users(:webmaster) + visit_club '/series/edit' + assert_selector 'h1', text: 'Edit Series' + + fill_in 'series[acronym]', with: 'SL' + fill_in 'series[name]', with: 'Summer League' + + # The color picker pops up from the input group addon + find('.color-picker .input-group-addon').click + assert_selector '.colorpicker.colorpicker-visible' + fill_in 'series[color]', with: 'rgba(0,128,0,1)' + find('h1', text: 'Edit Series').click + assert_no_selector '.colorpicker.colorpicker-visible' + + check 'series[is_current]' + click_button 'Save' + + assert_text 'The series has been updated.' + assert_selector 'h1', text: 'Series' + series = Series.find_by(club: clubs(:cluba), name: 'Summer League') + assert_not_nil series + assert_equal 'SL', series.acronym + assert_equal 'rgba(0,128,0,1)', series.color + assert series.is_current + + # The series color reaches the calendar legend through the series CSS + visit_club '/events/index' + legend_item = find(".series-legend li.series-#{series.id}", text: 'Summer League') + assert_equal 'rgb(0, 128, 0)', legend_item.evaluate_script('getComputedStyle(this).color') + end + + test "editing a series updates its color everywhere" do + series = series(:cluba_wednesday) + sign_in_to_club users(:webmaster) + visit_club '/series/index' + + within find('tr', text: 'Wednesday Evening Series') do + find('a[href="/series/edit/' + series.id.to_s + '"]').click + end + assert_selector 'h1', text: 'Edit Series' + # The color picker rehydrates the stored color (its HSB round trip may + # shift the value by a hair) + assert_match(/\A#ff6[56]00\z/, find_field('series[color]').value) + + # The picker keeps the stored format (hex here), so edit with a hex color + fill_in 'series[color]', with: '#ff00ff' + click_button 'Save' + + assert_text 'The series has been updated.' + assert_equal '#ff00ff', series.reload.color + + visit_club '/events/index' + legend_item = find(".series-legend li.series-#{series.id}", text: 'Wednesday Evening Series') + assert_equal 'rgb(255, 0, 255)', legend_item.evaluate_script('getComputedStyle(this).color') + end +end diff --git a/test/system/user_merge_test.rb b/test/system/user_merge_test.rb new file mode 100644 index 0000000..8660db7 --- /dev/null +++ b/test/system/user_merge_test.rb @@ -0,0 +1,34 @@ +require 'application_system_test_case' + +class UserMergeTest < ApplicationSystemTestCase + test "the duplicates screen merges a fake account into the real one" do + real = User.create!(name: 'Robin Runner', email: 'robin@example.com', + password: 'password123', password_confirmation: 'password123', + club: clubs(:cluba)) + fake = User.create_fake('Robin Runner') + result = Result.create!(user: fake, course: courses(:race_long), status: :ok) + + sign_in_to_club users(:webmaster) + visit_club '/users/showDuplicates' + assert_selector 'h1', text: 'Merge user accounts' + + # The webmaster's merge level does not include the manual merge form + assert_no_text 'Manual merge' + + # The real account is classified as primary, the fake one as duplicate + row = find('tr', text: 'Robin Runner') + row.assert_selector '.label', text: 'Real' + row.assert_selector '.label', text: 'Fake' + assert_equal real.id.to_s, row.all('td')[2].text + assert_equal fake.id.to_s, row.all('td')[6].text + + within(row) { click_button 'Merge' } + + # The pair is gone and the fake account's data now points at the real one + assert_selector 'h1', text: 'Merge user accounts' + assert_no_selector 'tr', text: 'Robin Runner' + assert_not User.exists?(fake.id) + assert User.exists?(real.id) + assert_equal real.id, result.reload.user_id + end +end From f1d860b8bf6eb3d491096974acd648a6557c43cb Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Sat, 1 Aug 2026 11:01:52 +0200 Subject: [PATCH 13/16] Fix access-control, XSS, and upload issues from the security audit - Block cross-tenant privilege escalation through user merge: a club admin can no longer merge a global-admin account (and inherit its cross-club rights or delete it). Adds User#global_admin?/#global_privilege_level. - Gate the globally shared map standards and roles behind a real global-admin privilege instead of any per-club level-100 administrator. - Scope course unregistration to the current course so a registration made on one course can't be used to unregister someone elsewhere; require a bounded, non-blank name for fake-user creation. - Restrict redactor uploads to an allowlisted set of extensions (stops stored XSS via .html/.svg), independent of the spoofable client content type. - Sanitize admin-authored rich text (pages, content blocks, series info) and escape club-supplied fields on the apex homepage. - Validate Map#file_url, neutralize CSV formula injection in the participation report, raise the minimum password length to 8, replace the committed Devise secret sample with a placeholder, and delete the dead update_result_list endpoint. - Add regression tests covering each of the above. --- .env.core.sample | 2 +- .../clubsite/content_blocks_controller.rb | 4 +- .../clubsite/courses_controller.rb | 9 ++- app/controllers/clubsite/users_controller.rb | 56 ++++++++++++++++--- app/controllers/redactor_controller.rb | 40 ++++++++----- app/controllers/results_controller.rb | 36 +----------- app/helpers/clubs_helper.rb | 13 ++++- app/helpers/clubsite/content_blocks_helper.rb | 6 +- app/helpers/home_helper.rb | 13 ++--- app/models/map.rb | 6 ++ app/models/user.rb | 20 +++++++ app/policies/map_standard_policy.rb | 10 ++-- app/policies/role_policy.rb | 6 +- app/views/clubsite/events/_info.html.erb | 2 +- app/views/clubsite/pages/display.html.erb | 2 +- config/initializers/0_devise.rb | 2 +- config/routes.rb | 2 - test/fixtures/files/redactor_sample.html | 1 + test/fixtures/files/redactor_sample.png | 1 + test/fixtures/files/redactor_sample.svg | 1 + test/helpers/home_helper_test.rb | 13 +++++ test/integration/clubsite_admin_test.rb | 44 +++++++++++++-- test/integration/clubsite_courses_test.rb | 52 +++++++++++++++++ .../integration/clubsite_form_methods_test.rb | 5 +- test/integration/clubsite_pages_test.rb | 30 ++++++++++ test/integration/clubsite_users_admin_test.rb | 45 +++++++++++++++ test/integration/redactor_upload_test.rb | 52 +++++++++++++++++ test/system/admin_crud_test.rb | 4 +- 28 files changed, 386 insertions(+), 91 deletions(-) create mode 100644 test/fixtures/files/redactor_sample.html create mode 100644 test/fixtures/files/redactor_sample.png create mode 100644 test/fixtures/files/redactor_sample.svg create mode 100644 test/helpers/home_helper_test.rb create mode 100644 test/integration/clubsite_courses_test.rb create mode 100644 test/integration/redactor_upload_test.rb diff --git a/.env.core.sample b/.env.core.sample index 439c843..7150220 100644 --- a/.env.core.sample +++ b/.env.core.sample @@ -7,7 +7,7 @@ WJR_FLICKR_API_KEY= WJR_PASSWORD_SALT= WJR_SECRET_TOKEN=secret-token WJR_SECRET_BASE=secret-base -WJR_DEVISE_SECRET_KEY=a3ce9067696ac6eb83a065b0298ec025a97af52d84872e84c9e9e622991fd89f5a2d4b9d20d2e391858ae07240d0c4600c3900a15158e7b90ddae4c20e68d92dm +WJR_DEVISE_SECRET_KEY=generate-with-bin-rails-secret-and-keep-out-of-git WJR_FACEBOOK_APP_ID= WJR_FACEBOOK_APP_SECRET= WJR_GOOGLE_APP_ID= diff --git a/app/controllers/clubsite/content_blocks_controller.rb b/app/controllers/clubsite/content_blocks_controller.rb index 191a88d..717e15c 100644 --- a/app/controllers/clubsite/content_blocks_controller.rb +++ b/app/controllers/clubsite/content_blocks_controller.rb @@ -8,7 +8,9 @@ def update authorize content_block content_block.update(content: params[:value]) # jEditable swaps the response into the block, so return the stored HTML. - render html: content_block.content.to_s.html_safe + # Sanitize it (script stripped, formatting kept) so stored content can't + # execute in the editor's or a visitor's browser. + render html: helpers.sanitize(content_block.content.to_s) end private diff --git a/app/controllers/clubsite/courses_controller.rb b/app/controllers/clubsite/courses_controller.rb index b042203..2977a4c 100644 --- a/app/controllers/clubsite/courses_controller.rb +++ b/app/controllers/clubsite/courses_controller.rb @@ -37,12 +37,15 @@ def register end # Removes a user's registration row for this course only. You can always - # unregister yourself; you can unregister someone else only if any of - # their existing registrations (on any course) was made by you. + # unregister yourself; you can unregister someone else only if their + # registration *on this course* was made by you. Scoping the relationship + # to this course stops a registration made on one course from being used to + # unregister that person from unrelated courses on other clubs' sites. def unregister course = find_course(params[:course_id]) user_id = target_user_id - unless user_id == current_user.id || Result.exists?(registrant_id: current_user.id, user_id: user_id) + unless user_id == current_user.id || + Result.exists?(course_id: course.id, user_id: user_id, registrant_id: current_user.id) raise Pundit::NotAuthorizedError end diff --git a/app/controllers/clubsite/users_controller.rb b/app/controllers/clubsite/users_controller.rb index f1979bd..c35adc4 100644 --- a/app/controllers/clubsite/users_controller.rb +++ b/app/controllers/clubsite/users_controller.rb @@ -20,9 +20,16 @@ def index # Creates a name-only fake user so that people without an account can be # registered. Responds with the new user's id as a bare JSON value, the - # contract expected by register-others.js and result-editor.js. + # contract expected by register-others.js and result-editor.js. A name is + # required (and length-bounded) so the endpoint can't be used to flood the + # users table with empty/garbage rows. def create - user = User.create_fake(params[:userName]) + name = params[:userName].to_s.strip + if name.blank? || name.length > 255 + return render json: { error: 'A valid name is required.' }, status: :unprocessable_entity + end + + user = User.create_fake(name) render json: user.id end @@ -37,9 +44,9 @@ def merge def show_duplicates authorize current_club, :show_duplicates?, policy_class: UserPolicy # Merge is a lower privilege level than edit. Merge-level users may only - # merge people associated with their club, whereas edit-level users can - # merge anyone. - @can_merge_any_user = UserPolicy.new(current_user, current_club).edit_any? + # merge people associated with their club, whereas edit-level and global + # admins can merge anyone (see #can_merge_any_user?). + @can_merge_any_user = can_merge_any_user? if request.post? if perform_merge(params.dig(:user, '0', :user_id), params.dig(:user, '1', :user_id)) @@ -61,20 +68,43 @@ def show_duplicates private - # Merges the source account into the target account. Missing users and - # merging an account into itself are ignored (matching the legacy - # behavior). Returns whether a merge happened. + # Merges the source account into the target account. Missing users, merging + # an account into itself, and merges the current user isn't allowed to + # perform are all silently ignored (matching the legacy behavior). Returns + # whether a merge happened. def perform_merge(target_id, source_id) return false if target_id.blank? || target_id.to_s == source_id.to_s target = User.find_by(id: target_id) source = User.find_by(id: source_id) return false if target.nil? || source.nil? + return false unless can_merge?(target) && can_merge?(source) Tools::UserMerge.merge(target, source) true end + # Guards each account in a merge. A merge re-points the source's privileges + # onto the target, so it is a privilege-granting operation and must not let + # a club admin absorb rights they don't already hold: + # * only a global admin may merge an account that holds a global + # (cross-club) privilege -- this is what stops a club webmaster from + # merging a platform administrator into their own account; + # * mergers who can't merge arbitrary accounts are restricted to accounts + # associated with the current club. + def can_merge?(user) + return false if user.global_admin? && !current_user.global_admin? + return true if can_merge_any_user? + + user_related_to_club?(user) + end + + def can_merge_any_user? + @can_merge_any_user ||= + current_user.global_admin? || + UserPolicy.new(current_user, current_club).edit_any? + end + # A duplicate entry concerns the current club when the account or its # most recent event belongs to the club def related_to_club?(entry) @@ -82,6 +112,16 @@ def related_to_club?(entry) entry[:most_recent_event]&.dig(:club_id) == current_club.id end + # Whether an account is associated with the current club: it belongs to the + # club, holds a membership, or has competed at one of the club's events. + def user_related_to_club?(user) + user.club_id == current_club.id || + Membership.exists?(user_id: user.id, club_id: current_club.id) || + Result.joins(course: :event) + .where(events: { club_id: current_club.id }, results: { user_id: user.id }) + .exists? + end + def require_sign_in return if user_signed_in? diff --git a/app/controllers/redactor_controller.rb b/app/controllers/redactor_controller.rb index f9cb7aa..26e2b17 100644 --- a/app/controllers/redactor_controller.rb +++ b/app/controllers/redactor_controller.rb @@ -5,37 +5,51 @@ class RedactorController < ApplicationController # since this request is coming via PHP, we don't have an authenticity token skip_before_action :verify_authenticity_token - def upload_image - data = params[:file] - image_types = ['image/png', 'image/jpg', 'image/gif', - 'image/jpeg', 'image/pjpeg'] - @url = nil - if image_types.include? data.content_type - @url = store_file(data) - end + # Extensions accepted by the rich-text "insert file" button. The stored file + # is served from the data host, and browsers pick how to render it from its + # extension -- so anything that can be interpreted as HTML/script (.html, + # .svg, .xml, .js, ...) is deliberately excluded to prevent stored XSS. + ALLOWED_FILE_EXTENSIONS = %w[ + png jpg jpeg gif webp + pdf txt csv + doc docx xls xlsx ppt pptx odt ods odp + zip + ].freeze + + # The "insert image" button is restricted further to raster image types. + ALLOWED_IMAGE_EXTENSIONS = %w[png jpg jpeg gif webp].freeze + def upload_image + @url = store_file(params[:file], ALLOWED_IMAGE_EXTENSIONS) respond_to :json, :html end def upload_file data = params[:file] - @url = store_file(data) - @filename = data.original_filename + @url = store_file(data, ALLOWED_FILE_EXTENSIONS) + @filename = data.original_filename if @url respond_to :json, :html end protected - def store_file(data) + # Stores an uploaded file under a random name, keeping only an allowlisted + # extension derived from the original filename. Returns the public URL, or + # nil when the upload is missing or its extension is not allowed. + def store_file(data, allowed_extensions) raise "not authorized" unless RedactorPolicy.new(current_user).store_file? - extension = File.extname(data.original_filename) + return nil if data.blank? + + extension = File.extname(data.original_filename.to_s).delete_prefix('.').downcase + return nil unless allowed_extensions.include?(extension) + root_path = Settings.dataFolder root_url = Settings.dataURL random = SecureRandom.urlsafe_base64 random_folder_1 = SecureRandom.random_number(9).to_s random_folder_2 = SecureRandom.random_number(9).to_s folder = 'files/' + random_folder_1 + "/" + random_folder_2 - relative_path = "%s/%s%s" % [folder, random, extension] + relative_path = "%s/%s.%s" % [folder, random, extension] # ensure the folder structure exists FileUtils.mkpath(root_path + folder) diff --git a/app/controllers/results_controller.rb b/app/controllers/results_controller.rb index e22c1db..b82e0ce 100644 --- a/app/controllers/results_controller.rb +++ b/app/controllers/results_controller.rb @@ -4,7 +4,7 @@ class ResultsController < ApplicationController before_action :authenticate_user!, :only => [:update_live_result_list, :process_result_list] # Result list uploads come from external tools authenticating over HTTP Basic, # which cannot supply a CSRF token. - skip_forgery_protection only: [:process_result_list, :update_live_result_list, :update_result_list] + skip_forgery_protection only: [:process_result_list, :update_live_result_list] def check_event_id id unless Event.exists? id @@ -33,39 +33,6 @@ def live_result_list end end - # TODO: This is the WIP replacement to process_result_list... - # Request body is the XML to store - # URL parameters - def update_result_list - # event_id = params[:id] - # resolutions = JSON.parse(param[:resolutions]) - # data = params[:file].read - event_id = 1404 - resolutions = [] - data = File.read('/tmp.xml') - - check_event_id event_id - data = ResultList.resolve_result_list_user_conflicts(resolutions, data) - - result_list = ResultList.find_or_initialize_by event_id: event_id, status: ResultList::FINAL_STATUS - authorize result_list - result_list.user_id = current_user.id - result_list.data = data - result_list.upload_time = Time.now - - # Check if there are any remaining unresolved users - remaining_conflicts = ResultList.result_list_user_conflicts event_id, data - if remaining_conflicts.length > 0 - # Reply with remaining conflicts. - elsif result_list.save - result_list.sync_result_list - else - # TODO handle validation error - end - - render :plain => remaining_conflicts.to_yaml - end - def result_list supported = ['2.0.3', '3.0'] unless supported.include? params[:iof_version] then @@ -85,7 +52,6 @@ def result_list end end - # TODO: this should have been replaced by update_result_list a long time ago. def process_result_list user = current_user event = Event.find(params[:id]) diff --git a/app/helpers/clubs_helper.rb b/app/helpers/clubs_helper.rb index dca71f8..298a9bb 100644 --- a/app/helpers/clubs_helper.rb +++ b/app/helpers/clubs_helper.rb @@ -13,9 +13,20 @@ def participant_counts_helper } series = event.series map = event.map - csv << [event.id, event.date, event.name, (series != nil) ? series.name : nil, event.number_of_participants, event.club.name, organizers.join(", "), map ? map.name : nil] + csv << [event.id, event.date, csv_safe(event.name), csv_safe(series&.name), event.number_of_participants, csv_safe(event.club.name), csv_safe(organizers.join(", ")), csv_safe(map&.name)] end end end + private + + # Neutralizes spreadsheet formula injection: a cell whose first character + # is one of = + - @ (or a leading tab/CR) is treated as a formula by Excel + # and Sheets, so prefix those values with an apostrophe. + def csv_safe(value) + str = value.to_s + str = "'" + str if str.match?(/\A[=+\-@\t\r]/) + str + end + end diff --git a/app/helpers/clubsite/content_blocks_helper.rb b/app/helpers/clubsite/content_blocks_helper.rb index 45bfd24..74342be 100644 --- a/app/helpers/clubsite/content_blocks_helper.rb +++ b/app/helpers/clubsite/content_blocks_helper.rb @@ -12,8 +12,10 @@ def render_content_blocks(key, start_wrapper = nil, end_wrapper = nil) safe_join(blocks.map do |block| parts = [] parts << start_wrapper.html_safe if start_wrapper - # Content is admin-authored HTML. - parts << content_tag(:div, block.content.to_s.html_safe, + # Content is admin-authored HTML; sanitize it so a content-block editor + # can't plant script that runs in a visitor's (or higher-privileged + # admin's) browser. sanitize keeps ordinary rich-text formatting. + parts << content_tag(:div, sanitize(block.content.to_s), id: "content-block-#{block.id}", class: css_class) parts << end_wrapper.html_safe if end_wrapper safe_join(parts) diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index ae9fec7..7a240b3 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -12,12 +12,11 @@ def render_club_tree(parent_clubs) end def formatted_clubs_list(clubs) - list = [] - clubs.each { |c| - str = '' + c.acronym + ' (' + c.location + ')' - list << str.html_safe - } - - list.to_sentence.html_safe + # Build the links with link_to/content_tag so the club-supplied acronym and + # location are HTML-escaped -- a club admin must not be able to inject + # markup onto the shared apex homepage. + clubs.map { |club| + link_to(club.url) { content_tag(:strong, "#{club.acronym} (#{club.location})") } + }.to_sentence.html_safe end end diff --git a/app/models/map.rb b/app/models/map.rb index 48f45d1..2f7162f 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -1,10 +1,16 @@ class Map < ApplicationRecord + # An http(s) (or scheme-less) web address. Constrains the admin-supplied + # file_url so maps/download can't be turned into a javascript:/data: or + # arbitrary-scheme redirect. Mirrors Event::URL_FORMAT. + URL_FORMAT = %r{\A(?:https?://)?(?:[\w-]+\.)+[a-z]{2,}(?::\d+)?(?:/\S*)?\z}i + has_many :event # Legacy records may not have a map standard assigned belongs_to :map_standard, optional: true belongs_to :club validates :name, presence: true + validates :file_url, format: { with: URL_FORMAT }, allow_blank: true def url club.clubsite_url("/maps/view/" + id.to_s) diff --git a/app/models/user.rb b/app/models/user.rb index 5b7da87..5090d27 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -204,6 +204,26 @@ def max_privilege_level end end + # Highest privilege level the user holds in global (club-independent) groups, + # i.e. groups with no club_id. Global privileges apply across every club and + # are the platform-wide admin roles. + def global_privilege_level + Privilege.joins(:user_group) + .where(user_id: id, groups: { club_id: nil }) + .maximum('groups.access_level') || 0 + end + + # True when the user holds a global (cross-club) privilege at or above the + # given level. + def has_global_privilege?(desired_privilege) + global_privilege_level >= desired_privilege + end + + # True when the user holds any global (cross-club) privilege. + def global_admin? + global_privilege_level.positive? + end + # True when the user holds a membership with the club for the given year def member_of?(club, year) Membership.exists?(user_id: id, club_id: club.id, year: year) diff --git a/app/policies/map_standard_policy.rb b/app/policies/map_standard_policy.rb index 3e4661f..a407c32 100644 --- a/app/policies/map_standard_policy.rb +++ b/app/policies/map_standard_policy.rb @@ -1,5 +1,7 @@ -# Map standards are shared between clubs, so the record is always the current -# club. +# Map standards are shared across every club (a club's maps reference them), so +# editing them is a cross-club operation and requires a global (club-independent) +# privilege -- a per-club administrator must not be able to rename or delete a +# standard that other clubs rely on. class MapStandardPolicy attr_reader :user, :club @@ -13,7 +15,7 @@ def index? end def edit? - user.present? && user.has_privilege?(Settings.privileges.mapStandard.edit, club) + user.present? && user.has_global_privilege?(Settings.privileges.mapStandard.edit) end def update? @@ -21,6 +23,6 @@ def update? end def destroy? - user.present? && user.has_privilege?(Settings.privileges.mapStandard.delete, club) + user.present? && user.has_global_privilege?(Settings.privileges.mapStandard.delete) end end diff --git a/app/policies/role_policy.rb b/app/policies/role_policy.rb index 32e9c47..8ab2c96 100644 --- a/app/policies/role_policy.rb +++ b/app/policies/role_policy.rb @@ -1,4 +1,6 @@ -# Roles are shared between clubs, so the record is always the current club. +# Roles are shared across every club (they populate every club's event +# organizer editor), so editing them is a cross-club operation and requires a +# global (club-independent) privilege. Listing stays open. class RolePolicy attr_reader :user, :club @@ -14,7 +16,7 @@ def index? end def edit? - user.present? && user.has_privilege?(Settings.privileges.role.edit, club) + user.present? && user.has_global_privilege?(Settings.privileges.role.edit) end def update? diff --git a/app/views/clubsite/events/_info.html.erb b/app/views/clubsite/events/_info.html.erb index 1f14a1c..eaa7400 100644 --- a/app/views/clubsite/events/_info.html.erb +++ b/app/views/clubsite/events/_info.html.erb @@ -33,7 +33,7 @@ <% if !event.completed? && event.series&.information.present? %>

    <%= event.series.name %>

    - <%= raw event.series.information %> + <%= sanitize event.series.information %>
    <% end %> diff --git a/app/views/clubsite/pages/display.html.erb b/app/views/clubsite/pages/display.html.erb index a3353b0..11dfd08 100644 --- a/app/views/clubsite/pages/display.html.erb +++ b/app/views/clubsite/pages/display.html.erb @@ -17,5 +17,5 @@
    - <%= raw @page.content %> + <%= sanitize @page.content %>
    diff --git a/config/initializers/0_devise.rb b/config/initializers/0_devise.rb index b932883..aeea712 100644 --- a/config/initializers/0_devise.rb +++ b/config/initializers/0_devise.rb @@ -131,7 +131,7 @@ # ==> Configuration for :validatable # Range for password length. Default is 8..128. - config.password_length = 6..128 + config.password_length = 8..128 # Email regex used to validate email formats. It simply asserts that # an one (and only one) @ exists in the given string. This is mainly diff --git a/config/routes.rb b/config/routes.rb index 3f64c96..19c4ef0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,8 +15,6 @@ get 'iof/:iof_version/clubs/:club_id/event_list', to: 'events#index', :constraints => version_constraint - # TODO TMP - get 'iof/result_list', to: 'results#update_result_list' get 'iof/:iof_version/events/:id/result_list', to: 'results#result_list', :constraints => version_constraint post 'iof/:iof_version/events/:id/result_list', to: 'results#process_result_list', :constraints => version_constraint post 'iof/:iof_version/events/:id/live_result_list', to: 'results#update_live_result_list', :constraints => version_constraint diff --git a/test/fixtures/files/redactor_sample.html b/test/fixtures/files/redactor_sample.html new file mode 100644 index 0000000..d58a4dc --- /dev/null +++ b/test/fixtures/files/redactor_sample.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/fixtures/files/redactor_sample.png b/test/fixtures/files/redactor_sample.png new file mode 100644 index 0000000..95ea00e --- /dev/null +++ b/test/fixtures/files/redactor_sample.png @@ -0,0 +1 @@ +PNG-bytes-not-really \ No newline at end of file diff --git a/test/fixtures/files/redactor_sample.svg b/test/fixtures/files/redactor_sample.svg new file mode 100644 index 0000000..1e56069 --- /dev/null +++ b/test/fixtures/files/redactor_sample.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/helpers/home_helper_test.rb b/test/helpers/home_helper_test.rb new file mode 100644 index 0000000..aab8f43 --- /dev/null +++ b/test/helpers/home_helper_test.rb @@ -0,0 +1,13 @@ +require 'test_helper' + +class HomeHelperTest < ActionView::TestCase + test 'formatted_clubs_list escapes club-supplied acronym and location' do + club = Club.new(url: 'https://cluba.example', acronym: '', + location: 'Town') + html = formatted_clubs_list([club]) + + assert_not_includes html, '' + assert_not_includes html, '' + assert_includes html, '<script>' + end +end diff --git a/test/integration/clubsite_admin_test.rb b/test/integration/clubsite_admin_test.rb index efcfe0d..08ba367 100644 --- a/test/integration/clubsite_admin_test.rb +++ b/test/integration/clubsite_admin_test.rb @@ -144,8 +144,8 @@ class ClubsiteAdminTest < ActionDispatch::IntegrationTest assert_equal expected, response.parsed_body.sort_by { |role| role['id'] } end - test 'role edit renders for the admin' do - sign_in users(:admin) + test 'role edit renders for a global admin' do + sign_in users(:global_admin) get "/roles/edit/#{roles(:organizer).id}" assert_response :success assert_select 'input#role_name[value=?]', 'Organizer' @@ -157,8 +157,16 @@ class ClubsiteAdminTest < ActionDispatch::IntegrationTest assert_redirected_to '/' end - test 'role edit without an id adds a new role' do + # Roles are shared across every club, so a per-club administrator (level 100 + # at their own club, but not a global admin) may not edit them. + test 'role edit redirects a per-club administrator' do sign_in users(:admin) + get '/roles/edit' + assert_redirected_to '/' + end + + test 'role edit without an id adds a new role for a global admin' do + sign_in users(:global_admin) assert_difference -> { Role.count }, 1 do post '/roles/edit', params: { role: { name: 'Controller', description: 'Controls the course' } } end @@ -166,10 +174,18 @@ class ClubsiteAdminTest < ActionDispatch::IntegrationTest assert Role.exists?(name: 'Controller') end + test 'a per-club administrator cannot add a role' do + sign_in users(:admin) + assert_no_difference -> { Role.count } do + post '/roles/edit', params: { role: { name: 'Controller', description: 'Controls the course' } } + end + assert_redirected_to '/' + end + # --- Map standards ------------------------------------------------------- - test 'map standards index renders for the admin' do - sign_in users(:admin) + test 'map standards index renders for a global admin' do + sign_in users(:global_admin) get '/mapStandards' assert_response :success assert_select 'h1', 'Map Standards' @@ -181,8 +197,16 @@ class ClubsiteAdminTest < ActionDispatch::IntegrationTest assert_redirected_to '/' end - test 'map standard add, update and delete' do + # Map standards are shared across every club, so a per-club administrator may + # not manage them. + test 'map standards index redirects a per-club administrator' do sign_in users(:admin) + get '/mapStandards' + assert_redirected_to '/' + end + + test 'map standard add, update and delete by a global admin' do + sign_in users(:global_admin) assert_difference -> { MapStandard.count }, 1 do post '/mapStandards/edit', params: { map_standard: { name: 'ISOM 2017', color: 'rgba(0,0,0,1)', description: 'Orienteering maps' @@ -209,6 +233,14 @@ class ClubsiteAdminTest < ActionDispatch::IntegrationTest assert_redirected_to '/' end + test 'a per-club administrator cannot delete a map standard' do + sign_in users(:admin) + assert_no_difference -> { MapStandard.count } do + post '/mapStandards/delete/1' + end + assert_redirected_to '/' + end + # --- Memberships --------------------------------------------------------- test 'memberships index renders for a plain signed-in member' do diff --git a/test/integration/clubsite_courses_test.rb b/test/integration/clubsite_courses_test.rb new file mode 100644 index 0000000..200bc08 --- /dev/null +++ b/test/integration/clubsite_courses_test.rb @@ -0,0 +1,52 @@ +require 'test_helper' + +class ClubsiteCoursesTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + setup do + host! 'cluba.test' + @registrant = users(:member) + @victim = users(:organizer) + @registered_course = courses(:race_long) + @other_course = courses(:race_score) + end + + test 'a registrant can unregister someone from the course they registered them on' do + sign_in @registrant + Result.create!(course: @registered_course, user: @victim, registrant: @registrant) + + post "/courses/unregister/#{@registered_course.id}/#{@victim.id}" + + assert_not Result.exists?(course_id: @registered_course.id, user_id: @victim.id) + end + + test 'a registration on one course cannot be used to unregister from another course' do + sign_in @registrant + # The registrant registered the victim on one course... + Result.create!(course: @registered_course, user: @victim, registrant: @registrant) + # ...but the victim's registration on a different course was made by someone else. + foreign = Result.create!(course: @other_course, user: @victim, registrant: @victim) + + post "/courses/unregister/#{@other_course.id}/#{@victim.id}" + + assert_redirected_to '/' + assert Result.exists?(foreign.id), 'the unrelated registration must be left intact' + end + + test 'anyone can unregister themselves' do + sign_in @victim + mine = Result.create!(course: @other_course, user: @victim, registrant: @victim) + + post "/courses/unregister/#{@other_course.id}/#{@victim.id}" + + assert_not Result.exists?(mine.id) + end + + test 'creating a fake user requires a non-blank name' do + sign_in @registrant + assert_no_difference -> { User.count } do + post '/users/add', params: { userName: ' ' } + end + assert_response :unprocessable_entity + end +end diff --git a/test/integration/clubsite_form_methods_test.rb b/test/integration/clubsite_form_methods_test.rb index 3d5677c..4a5bee5 100644 --- a/test/integration/clubsite_form_methods_test.rb +++ b/test/integration/clubsite_form_methods_test.rb @@ -8,7 +8,10 @@ class ClubsiteFormMethodsTest < ActionDispatch::IntegrationTest setup do host! 'cluba.test' - sign_in users(:admin) + # A global admin satisfies both the club-scoped checks (via privilege_level, + # which includes global groups) and the global-only map standard / role + # gates, so it can exercise every edit route here. + sign_in users(:global_admin) end test "map edit form submits via PATCH" do diff --git a/test/integration/clubsite_pages_test.rb b/test/integration/clubsite_pages_test.rb index 400ec2f..d9ce49b 100644 --- a/test/integration/clubsite_pages_test.rb +++ b/test/integration/clubsite_pages_test.rb @@ -196,4 +196,34 @@ class ClubsitePagesTest < ActionDispatch::IntegrationTest end assert_response :redirect end + + # --- Stored XSS is stripped from rendered rich text --- + + test 'content block script content is sanitized when rendered' do + block = content_blocks(:cluba_general_information) + block.update_column(:content, '

    hello

    ') + get '/' + assert_response :success + assert_no_match %r{}, response.body + assert_select "div#content-block-#{block.id}", html: %r{hello} + end + + test 'editing a content block returns sanitized html' do + sign_in users(:executive) + block = content_blocks(:cluba_general_information) + post '/contentBlocks/edit', + params: { id: "content-block-#{block.id}", value: '

    ok

    ' } + assert_response :success + assert_no_match %r{') + get "/pages/#{page.id}" + assert_response :success + assert_no_match %r{}, response.body + assert_select "div#page-resource-#{page.id}", html: %r{safe} + end end diff --git a/test/integration/clubsite_users_admin_test.rb b/test/integration/clubsite_users_admin_test.rb index 4619fb4..67bc5bc 100644 --- a/test/integration/clubsite_users_admin_test.rb +++ b/test/integration/clubsite_users_admin_test.rb @@ -129,6 +129,51 @@ class ClubsiteUsersAdminTest < ActionDispatch::IntegrationTest assert_redirected_to '/' end + test 'a club webmaster cannot merge a global admin into another account' do + sign_in users(:webmaster) + assert_no_difference -> { User.count } do + post "/users/merge/#{users(:member).id}/#{users(:global_admin).id}" + end + assert User.exists?(users(:global_admin).id) + end + + test 'even a club administrator cannot merge a global admin (no cross-club privilege theft)' do + # The club admin passes can_merge_any_user?, so only the global-admin guard + # stops the merge -- this is the CRITICAL escalation path. + sign_in users(:admin) + assert_no_difference -> { User.count } do + post "/users/merge/#{users(:admin).id}/#{users(:global_admin).id}" + end + assert User.exists?(users(:global_admin).id) + assert_equal 0, users(:admin).global_privilege_level + end + + test 'a club webmaster cannot merge accounts unrelated to their club' do + sign_in users(:webmaster) + target = User.create_fake('Unrelated One') + source = User.create_fake('Unrelated Two') + target.update_columns(club_id: clubs(:clubb).id) + source.update_columns(club_id: clubs(:clubb).id) + + assert_no_difference -> { User.count } do + post "/users/merge/#{target.id}/#{source.id}" + end + assert User.exists?(source.id) + end + + test 'a global admin may merge accounts unrelated to any single club' do + sign_in users(:global_admin) + target = User.create_fake('Unrelated One') + source = User.create_fake('Unrelated Two') + target.update_columns(club_id: clubs(:clubb).id) + source.update_columns(club_id: clubs(:clubb).id) + + assert_difference -> { User.count }, -1 do + post "/users/merge/#{target.id}/#{source.id}" + end + assert_not User.exists?(source.id) + end + # --- Show duplicates --- test 'showDuplicates requires the merge privilege' do diff --git a/test/integration/redactor_upload_test.rb b/test/integration/redactor_upload_test.rb new file mode 100644 index 0000000..214d68e --- /dev/null +++ b/test/integration/redactor_upload_test.rb @@ -0,0 +1,52 @@ +require 'test_helper' + +class RedactorUploadTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + setup do + # organizer has an Organizer row, so RedactorPolicy#store_file? passes + sign_in users(:organizer) + end + + test 'upload_file stores an allowlisted extension' do + post '/api/redactor/uploadFile', + params: { file: fixture_file_upload('redactor_sample.png', 'image/png') } + assert_response :success + assert JSON.parse(response.body)['filelink'].present? + end + + test 'upload_file rejects an html file even with a spoofed image content-type' do + post '/api/redactor/uploadFile', + params: { file: fixture_file_upload('redactor_sample.html', 'image/png') } + assert_response :success + assert_nil JSON.parse(response.body)['filelink'] + end + + test 'upload_file rejects an svg file' do + post '/api/redactor/uploadFile', + params: { file: fixture_file_upload('redactor_sample.svg', 'image/svg+xml') } + assert_response :success + assert_nil JSON.parse(response.body)['filelink'] + end + + test 'upload_image accepts a png' do + post '/api/redactor/uploadImage', + params: { file: fixture_file_upload('redactor_sample.png', 'image/png') } + assert_response :success + assert JSON.parse(response.body)['filelink'].present? + end + + test 'upload_image rejects a non-image extension' do + post '/api/redactor/uploadImage', + params: { file: fixture_file_upload('redactor_sample.html', 'image/png') } + assert_response :success + assert_nil JSON.parse(response.body)['filelink'] + end + + test 'redactor uploads require sign-in' do + sign_out :user + post '/api/redactor/uploadImage', + params: { file: fixture_file_upload('redactor_sample.png', 'image/png') } + assert_response :redirect + end +end diff --git a/test/system/admin_crud_test.rb b/test/system/admin_crud_test.rb index 95bac03..fd4a60f 100644 --- a/test/system/admin_crud_test.rb +++ b/test/system/admin_crud_test.rb @@ -64,7 +64,7 @@ class AdminCrudTest < ApplicationSystemTestCase test "editing a map standard" do map_standard = MapStandard.create!(name: 'ISOM', color: 'rgba(0,0,0,1)') - sign_in_to_club users(:admin) + sign_in_to_club users(:global_admin) visit_club '/mapStandards/' assert_selector 'h1', text: 'Map Standards' @@ -89,7 +89,7 @@ class AdminCrudTest < ApplicationSystemTestCase test "editing a role" do role = roles(:course_planner) - sign_in_to_club users(:admin) + sign_in_to_club users(:global_admin) visit_club '/roles/' assert_selector 'h1', text: 'Roles' From 5c1b44242ca8c537e82184f4d8268ee54622ce7f Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Sat, 1 Aug 2026 11:02:11 +0200 Subject: [PATCH 14/16] Replace the database session store with the encrypted cookie store activerecord-session_store serialized session data with Marshal, so any database-write foothold (restored backup, compromised replica, a future SQLi) could be escalated to RCE via Marshal.load on the next request. The DB store was only there for the legacy cross-app setup, but the CakePHP app uses file-based sessions and never reads Rails' sessions table (the bridge rides on the separate cross_app_sessions token table), so the store can be removed. - Switch to the encrypted :cookie_store (secure + httponly via force_ssl). - Remove the activerecord-session_store gem, drop the sessions table, and remove the now-unused sessions:trim cron job. Deploy note: the session cookie key changes, so users are signed out once. --- Gemfile | 1 - Gemfile.lock | 9 --------- config/initializers/session_store.rb | 11 +++++------ config/schedule.rb | 4 ---- .../20260731120001_drop_sessions_table.rb | 18 ++++++++++++++++++ db/schema.rb | 13 ++----------- lib/tasks/sessions.rake | 9 --------- 7 files changed, 25 insertions(+), 40 deletions(-) create mode 100644 db/migrate/20260731120001_drop_sessions_table.rb delete mode 100644 lib/tasks/sessions.rake diff --git a/Gemfile b/Gemfile index 1678ed1..5516700 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gem 'mysql2', '~> 0.5.3' gem 'puma', '~> 6.4' # MySQL Session store -gem "activerecord-session_store", "~> 2.1.0" # Asset pipeline gem 'sass-rails', '~> 6.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index dfff5f5..7fda6fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,13 +56,6 @@ GEM activemodel (= 7.2.3.1) activesupport (= 7.2.3.1) timeout (>= 0.4.0) - activerecord-session_store (2.1.0) - actionpack (>= 6.1) - activerecord (>= 6.1) - cgi (>= 0.3.6) - multi_json (~> 1.11, >= 1.11.2) - rack (>= 2.0.8, < 4) - railties (>= 6.1) activestorage (7.2.3.1) actionpack (= 7.2.3.1) activejob (= 7.2.3.1) @@ -187,7 +180,6 @@ GEM mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.27.0) - multi_json (1.15.0) mysql2 (0.5.6) net-imap (0.4.20) date @@ -330,7 +322,6 @@ PLATFORMS x86_64-linux DEPENDENCIES - activerecord-session_store (~> 2.1.0) bcrypt (~> 3.1.22) bootstrap-sass (~> 3.4.1) brakeman diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 90db0e5..c370bb8 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,8 +1,7 @@ # Be sure to restart your server when you modify this file. -#WhyJustRun::Application.config.session_store :cookie_store, key: '_WhyJustRun_session' - -# Use the database for sessions instead of the cookie-based default, -# which shouldn't be used to store highly confidential information -# (create the session table with "rails generate session_migration") -WhyJustRun::Application.config.session_store :active_record_store +# Store the session in an encrypted, signed cookie (the Rails default). The +# session only holds small values (return paths, a redirect club id, the signed +# in user id), so it fits comfortably in the cookie, and there is no server-side +# session table to serialize, sweep, or protect. +WhyJustRun::Application.config.session_store :cookie_store, key: '_whyjustrun_session' diff --git a/config/schedule.rb b/config/schedule.rb index b9a3a3d..b2e7c3c 100644 --- a/config/schedule.rb +++ b/config/schedule.rb @@ -7,7 +7,3 @@ job_type :rake, "cd :path && :environment_variable=:environment :bundle_command rake :task :output" -every 1.day do - rake "sessions:trim" -end - diff --git a/db/migrate/20260731120001_drop_sessions_table.rb b/db/migrate/20260731120001_drop_sessions_table.rb new file mode 100644 index 0000000..184328e --- /dev/null +++ b/db/migrate/20260731120001_drop_sessions_table.rb @@ -0,0 +1,18 @@ +# The app now uses the encrypted cookie session store instead of the database +# store (see config/initializers/session_store.rb), so the server-side sessions +# table is no longer used. Reversible: down recreates the table as it was. +class DropSessionsTable < ActiveRecord::Migration[7.2] + def up + drop_table :sessions if table_exists?(:sessions) + end + + def down + create_table :sessions do |t| + t.string :session_id, null: false + t.text :data + t.timestamps null: false + end + add_index :sessions, :session_id + add_index :sessions, :updated_at + end +end diff --git a/db/schema.rb b/db/schema.rb index f23385f..f225c8f 100755 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_07_31_185542) do +ActiveRecord::Schema[7.2].define(version: 2026_07_31_120001) do create_table "club_categories", id: :integer, charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name" end @@ -58,7 +58,7 @@ t.index ["event_id"], name: "event_id" end - create_table "cross_app_sessions", id: :integer, charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| + create_table "cross_app_sessions", id: :integer, charset: "utf8mb3", collation: "utf8mb3_general_ci", force: :cascade do |t| t.integer "user_id", null: false t.string "cross_app_session_id", null: false t.datetime "created_at", precision: nil, null: false @@ -235,15 +235,6 @@ t.index ["club_id"], name: "club_id" end - create_table "sessions", id: :integer, charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| - t.string "session_id", null: false - t.text "data" - t.datetime "created_at", precision: nil, null: false - t.datetime "updated_at", precision: nil, null: false - t.index ["session_id"], name: "index_sessions_on_session_id" - t.index ["updated_at"], name: "index_sessions_on_updated_at" - end - create_table "short_links", id: :integer, charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name" t.string "destination" diff --git a/lib/tasks/sessions.rake b/lib/tasks/sessions.rake deleted file mode 100644 index 5f2d190..0000000 --- a/lib/tasks/sessions.rake +++ /dev/null @@ -1,9 +0,0 @@ -namespace :sessions do - desc "Clear expired sessions" - task :trim => :environment do - puts "Clearing expired sessions..." - sql = 'DELETE FROM sessions WHERE updated_at < DATE_SUB(NOW(), INTERVAL 90 DAY);' - ApplicationRecord.connection.execute(sql) - puts "Done clearing sessions" - end -end From 453f1ce4e28d306fa2d86b37e3fd3355e3d7811e Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Sat, 1 Aug 2026 12:18:50 +0200 Subject: [PATCH 15/16] Capture page state and upload artifacts when system tests fail on CI The system tests fail on CI but pass locally, and the failure assertion alone (wrong host after the SSO handoff) does not show which redirect branch was taken. Save the final URL and page HTML next to the automatic failure screenshot and upload the folder as a workflow artifact. --- .github/workflows/ci.yml | 8 ++++++++ test/application_system_test_case.rb | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e29e485..c7d391c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,6 +116,14 @@ jobs: - name: Run system tests run: bundle exec rails test:system + - name: Upload failure screenshots and pages + uses: actions/upload-artifact@v4 + if: failure() + with: + name: system-test-failures + path: tmp/screenshots/ + if-no-files-found: ignore + brakeman: runs-on: ubuntu-latest steps: diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 94b2c72..35dc543 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -48,6 +48,7 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase ].freeze teardown do + save_failure_page unless passed? raise_on_console_errors if passed? end @@ -95,6 +96,18 @@ def wait_for_condition(timeout: Capybara.default_max_wait_time, message: 'condit private + # Companion to the automatic failure screenshot: the final URL and page HTML + # make redirect-flow failures diagnosable from CI artifacts. + def save_failure_page + dir = Rails.root.join('tmp/screenshots') + FileUtils.mkdir_p(dir) + slug = "failures_#{method_name.parameterize(separator: '_')}" + File.write(dir.join("#{slug}.url.txt"), "#{current_url}\n") + File.write(dir.join("#{slug}.html"), page.html) + rescue StandardError => e + warn "Could not save failure page: #{e.message}" + end + def raise_on_console_errors logs = page.driver.browser.logs.get(:browser) ignored = IGNORED_CONSOLE_ERRORS + (@allowed_console_errors || []) From 3add5c4fd9eff37aab3338378fd681f788827962 Mon Sep 17 00:00:00 2001 From: Russell Porter Date: Sat, 1 Aug 2026 12:31:07 +0200 Subject: [PATCH 16/16] Fix system-test races that fail on newer Chrome The CI failures all shared one shape: after signing in on the apex domain, the next visit to a club domain was silently dropped and the test asserted against the apex page. The sign-in submit goes through rails-ujs, so the driver's click does not wait for the resulting navigation; sign_in_through_browser now waits for the signed-in page before returning. CI's Chrome 151 loses that race consistently while the Docker image's Chromium 131 happens to win it. Also de-flake the registrant comment tests: Bootstrap's modal fade animation moves the dialog while the click position is computed, so the Save click could land on the backdrop and dismiss the modal without submitting. Disable animations in system tests and wait on the saved comment instead of on a header that is already visible under the modal. The failure-page capture now runs in before_teardown, while the page is still live, instead of after Capybara has reset the session. --- test/application_system_test_case.rb | 18 +++++++++++++++++- test/system/registrant_comment_test.rb | 14 +++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index 35dc543..ff1223f 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -25,6 +25,11 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase Capybara.always_include_port = true + # Bootstrap's fade animations move elements while Capybara computes click + # coordinates, so clicks can land on the backdrop and dismiss modals + # instead of hitting their buttons. + Capybara.disable_animation = true + # The clubsite sign-in flow bounces through absolute apex URLs # (Settings.coreURL), so the apex URL must point at the test server. The # server port is fixed so the URL can be built before the server boots. @@ -48,10 +53,16 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase ].freeze teardown do - save_failure_page unless passed? raise_on_console_errors if passed? end + # Runs before the framework's screenshot-and-reset hook: the page is still + # live here, so the captured URL and HTML reflect the failure state. + def before_teardown + save_failure_page unless passed? + super + end + def allow_console_errors(pattern) (@allowed_console_errors ||= []) << pattern end @@ -73,6 +84,11 @@ def sign_in_through_browser(user, password: 'password123') fill_in 'user[password]', with: password click_button 'Sign in' end + # The submit goes through rails-ujs, so the driver's click does not wait + # for the resulting navigation. Wait for the signed-in page before + # returning, or a caller's next visit races the in-flight redirect and can + # be silently dropped. + assert_selector 'a', text: 'Sign out' end # Signs in on the apex domain, then follows the SSO handoff so the club diff --git a/test/system/registrant_comment_test.rb b/test/system/registrant_comment_test.rb index cb80fba..e00e1b4 100644 --- a/test/system/registrant_comment_test.rb +++ b/test/system/registrant_comment_test.rb @@ -17,9 +17,12 @@ class RegistrantCommentTest < ApplicationSystemTestCase click_button 'Save' end - # Saving posts the comment and returns to the event page - assert_selector 'h1', text: 'Spring Sprint' - assert_equal 'Can offer a ride from downtown', result.reload.registrant_comment + # Saving posts the comment and returns to the event page. The event page + # is already visible under the modal, so wait on the database effect + # rather than on a page element. + wait_for_condition(message: 'comment was not saved') do + result.reload.registrant_comment == 'Can offer a ride from downtown' + end # The entry now offers editing and shows the comment bubble within find('tr', text: 'Mary Member') do @@ -47,7 +50,8 @@ class RegistrantCommentTest < ApplicationSystemTestCase click_button 'Save' end - assert_selector 'h1', text: 'Spring Sprint' - assert_equal 'Ride offer withdrawn', result.reload.registrant_comment + wait_for_condition(message: 'comment was not saved') do + result.reload.registrant_comment == 'Ride offer withdrawn' + end end end