diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2f50b14a0..9ed258cc8 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,7 +1,7 @@ FROM mcr.microsoft.com/devcontainers/ruby:0-3.1-bullseye # Install Rails -RUN gem install rails:7.0.7 webdrivers:5.2.0 +RUN gem install rails:7.0.8 # Default value to allow debug server to serve content over GitHub Codespace's port forwarding service # The value is a comma-separated list of allowed domains @@ -12,8 +12,12 @@ ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.git #RUN bundle exec rake db:migrate # [Optional] Uncomment this section to install additional OS packages. -# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ -# && apt-get -y install --no-install-recommends +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends bash-completion + +# Chrome for testing packages. https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#chrome-doesnt-launch-on-linux +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils # [Optional] Uncomment this line to install additional gems. # RUN gem install diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 940d241e7..50688ae72 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,7 +14,7 @@ // Use 'forwardPorts' to make a list of ports inside the container available locally. // This can be used to network with other containers or the host. -"forwardPorts": [3000, 5432, 9200], +"forwardPorts": [3000, 5432, 9200, 8081], // Use 'postCreateCommand' to run commands after the container is created. // these don't actually work as postCreateCommands, you need to run them manually diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69a2e1376..167bc6020 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,7 +76,7 @@ jobs: id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - name: Setup yarn cache - uses: actions/cache@v3 + uses: actions/cache@v4 id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} diff --git a/.rubocop.yml b/.rubocop.yml index 52bf41aeb..b42b06f04 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,7 +7,7 @@ AllCops: Exclude: - 'db/schema.rb' - 'vendor/**/*' - TargetRailsVersion: 6.0 + TargetRailsVersion: 7.0 Rails: Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 122335cbf..4fa490610 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -48,12 +48,6 @@ FactoryBot/CreateList: - 'spec/views/places/show.html.haml_spec.rb' - 'spec/views/posts/index.html.haml_spec.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -FactoryBot/RedundantFactoryOption: - Exclude: - - 'spec/factories/scientific_name.rb' - # Offense count: 1135 # This cop supports unsafe autocorrection (--autocorrect-all). FactoryBot/SyntaxMethods: @@ -101,15 +95,6 @@ Layout/LineEndStringConcatenationIndentation: Layout/LineLength: Max: 304 -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceBeforeBlockBraces: - Exclude: - - 'spec/models/photo_spec.rb' - # Offense count: 3 # This cop supports safe autocorrection (--autocorrect). Lint/AmbiguousOperatorPrecedence: @@ -242,15 +227,6 @@ RSpec/EmptyExampleGroup: - 'spec/views/photos/edit.html.haml_spec.rb' - 'spec/views/posts/_single.html.haml_spec.rb' -# Offense count: 6 -# This cop supports safe autocorrection (--autocorrect). -RSpec/EmptyLineAfterExampleGroup: - Exclude: - - 'spec/features/crops/creating_a_crop_spec.rb' - - 'spec/features/likeable_spec.rb' - - 'spec/models/crop_spec.rb' - - 'spec/support/feature_helpers.rb' - # Offense count: 134 # Configuration parameters: CountAsOne. RSpec/ExampleLength: @@ -323,13 +299,6 @@ RSpec/MessageChain: RSpec/MessageSpies: EnforcedStyle: receive -# Offense count: 22 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: hash, symbol -RSpec/MetadataStyle: - Enabled: false - # Offense count: 1 RSpec/MultipleDescribes: Exclude: diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5362e2f72..b0f3e35db 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -97,5 +97,6 @@ submit the change with your pull request. ## Bots ### Security and Dependency Updates +- `codefactor-io[bot]` - DeppBot / [deppbot](https://github.com/deppbot) - `dependabot[bot]` [dependabot](https://github.com/dependabot-bot) / [dependabot-preview](https://github.com/apps/dependabot-preview) diff --git a/Gemfile b/Gemfile index f54121c5b..0e7367e83 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ source 'https://rubygems.org' # Match ruby version in .ruby-version ruby File.read('.ruby-version') -gem 'rails', '~> 7.0.7' +gem 'rails', '~> 7.1.0' # Keeping old sprockets # https://github.com/rails/sprockets-rails/issues/444#issuecomment-637817050 @@ -33,18 +33,18 @@ gem 'material_icons' # icons gem 'font-awesome-sass' -gem 'uglifier' # JavaScript compressor +gem 'terser' gem 'oj' # Speeds up json # planting and harvest predictions # based on median values for the crop -gem 'active_median', '0.2.0' +gem 'active_median' gem 'active_record_union' gem 'flickraw' gem 'jquery-rails' -gem 'jquery-ui-rails' +gem 'jquery-ui-rails', github: 'jquery-ui-rails/jquery-ui-rails', tag: 'v7.0.0' # See https://github.com/jquery-ui-rails/jquery-ui-rails/issues/146 gem 'cancancan' # for checking member privileges gem 'csv_shaper' # CSV export @@ -100,7 +100,7 @@ gem 'omniauth-twitter' gem "chartkick" # clever elastic search -gem 'elasticsearch', '< 7.0.0' +gem 'elasticsearch', '~> 7.0.0' gem 'searchkick' gem "hashie", ">= 3.5.3" @@ -124,10 +124,19 @@ gem 'rack-protection', '>= 2.0.1' gem 'mailboxer', '>= 0.15.1' gem 'faraday' -gem 'faraday_middleware' gem 'rack-cors' +gem 'icalendar' + +# for signups as requested by email service +gem 'recaptcha' + +# External APIs for data +gem "gbifrb" + +gem "msgpack" + group :production do gem 'bonsai-elasticsearch-rails' # Integration with Bonsa-Elasticsearch on heroku gem 'dalli' @@ -153,6 +162,7 @@ group :development, :test do gem 'factory_bot_rails' # for creating test data gem 'faker' gem 'haml-rails' # HTML templating language + gem 'pry' gem 'query_diet' gem 'rspec-activemodel-mocks' gem 'rspec-rails' # unit testing framework @@ -172,11 +182,13 @@ group :development, :test do end group :test do + gem 'axe-core-capybara' + gem 'axe-core-rspec' gem 'codeclimate-test-reporter', require: false gem 'rails-controller-testing' gem 'selenium-webdriver' gem 'timecop' - gem 'webdrivers' + gem 'vcr' end group :travis do diff --git a/Gemfile.lock b/Gemfile.lock index 5b05638ed..c3f2263d8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,11 @@ +GIT + remote: https://github.com/jquery-ui-rails/jquery-ui-rails.git + revision: 413265e81f790f795239e07e7e25e01429b2f18d + tag: v7.0.0 + specs: + jquery-ui-rails (7.0.0) + railties (>= 3.2.16) + GIT remote: https://github.com/restarone/comfortable-mexican-sofa.git revision: ccf9415ae220453a199759b8ecbb8e9436c75c85 @@ -25,86 +33,110 @@ GEM GEM remote: https://rubygems.org/ specs: - actioncable (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) + actioncable (7.1.3) + actionpack (= 7.1.3) + activesupport (= 7.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + zeitwerk (~> 2.6) + actionmailbox (7.1.3) + actionpack (= 7.1.3) + activejob (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8) - actionpack (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activesupport (= 7.0.8) + actionmailer (7.1.3) + actionpack (= 7.1.3) + actionview (= 7.1.3) + activejob (= 7.1.3) + activesupport (= 7.1.3) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp - rails-dom-testing (~> 2.0) - actionpack (7.0.8) - actionview (= 7.0.8) - activesupport (= 7.0.8) - rack (~> 2.0, >= 2.2.4) + rails-dom-testing (~> 2.2) + actionpack (7.1.3) + actionview (= 7.1.3) + activesupport (= 7.1.3) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) rack-test (>= 0.6.3) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.8) - actionpack (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actiontext (7.1.3) + actionpack (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8) - activesupport (= 7.0.8) + actionview (7.1.3) + activesupport (= 7.1.3) builder (~> 3.1) - erubi (~> 1.4) - rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.1, >= 1.2.0) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) active_link_to (1.0.5) actionpack addressable - active_median (0.2.0) - activerecord (>= 4.2) + active_median (0.4.1) + activesupport (>= 6.1) active_record_union (1.3.0) activerecord (>= 4.0) active_utils (3.4.1) activesupport (>= 4.2) i18n - activejob (7.0.8) - activesupport (= 7.0.8) + activejob (7.1.3) + activesupport (= 7.1.3) globalid (>= 0.3.6) - activemodel (7.0.8) - activesupport (= 7.0.8) - activerecord (7.0.8) - activemodel (= 7.0.8) - activesupport (= 7.0.8) - activestorage (7.0.8) - actionpack (= 7.0.8) - activejob (= 7.0.8) - activerecord (= 7.0.8) - activesupport (= 7.0.8) + activemodel (7.1.3) + activesupport (= 7.1.3) + activerecord (7.1.3) + activemodel (= 7.1.3) + activesupport (= 7.1.3) + timeout (>= 0.4.0) + activestorage (7.1.3) + actionpack (= 7.1.3) + activejob (= 7.1.3) + activerecord (= 7.1.3) + activesupport (= 7.1.3) marcel (~> 1.0) - mini_mime (>= 1.1.0) - activesupport (7.0.8) + activesupport (7.1.3) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) autoprefixer-rails (10.4.7.0) execjs (~> 2) + axe-core-api (4.8.1) + dumb_delegator + virtus + axe-core-capybara (4.8.1) + axe-core-api + dumb_delegator + axe-core-rspec (4.8.1) + axe-core-api + dumb_delegator + virtus + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + base64 (0.2.0) bcrypt (3.1.19) better_errors (2.10.1) erubi (>= 1.0.0) @@ -117,7 +149,7 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bigdecimal (3.1.4) + bigdecimal (3.1.6) bluecloth (2.2.0) bonsai-elasticsearch-rails (7.0.1) elasticsearch-model (< 8) @@ -132,16 +164,16 @@ GEM actionpack (>= 5.2) activemodel (>= 5.2) builder (3.2.4) - bullet (7.1.4) + bullet (7.1.6) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) byebug (11.1.3) cancancan (3.5.0) - capybara (3.39.2) + capybara (3.40.0) addressable matrix mini_mime (>= 0.1.3) - nokogiri (~> 1.8) + nokogiri (~> 1.11) rack (>= 1.6.0) rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) @@ -152,17 +184,19 @@ GEM capybara-screenshot (1.0.26) capybara (>= 1.0, < 4) launchy - carrierwave (2.1.1) - activemodel (>= 5.0.0) - activesupport (>= 5.0.0) + carrierwave (3.0.5) + activemodel (>= 6.0.0) + activesupport (>= 6.0.0) addressable (~> 2.6) image_processing (~> 1.1) - mimemagic (>= 0.3.0) - mini_mime (>= 0.1.3) + marcel (~> 1.0.0) ssrf_filter (~> 1.0) chartkick (5.0.5) codeclimate-test-reporter (1.0.9) simplecov (<= 0.13) + coderay (1.1.3) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) coffee-rails (5.0.0) coffee-script (>= 2.2.0) railties (>= 5.2.0) @@ -172,19 +206,22 @@ GEM coffee-script-source (1.12.2) comfy_bootstrap_form (4.0.9) rails (>= 5.0.0) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) connection_pool (2.4.1) crass (1.0.6) csv_shaper (1.3.2) activesupport (>= 3.0.0) - dalli (3.2.6) + dalli (3.2.7) + base64 database_cleaner (2.0.2) database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (2.1.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.3.3) + date (3.3.4) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -199,61 +236,44 @@ GEM dotenv-rails (2.8.1) dotenv (= 2.8.1) railties (>= 3.2) - elasticsearch (6.8.3) - elasticsearch-api (= 6.8.3) - elasticsearch-transport (= 6.8.3) - elasticsearch-api (6.8.3) + drb (2.2.0) + ruby2_keywords + dumb_delegator (1.0.0) + elasticsearch (7.0.0) + elasticsearch-api (= 7.0.0) + elasticsearch-transport (= 7.0.0) + elasticsearch-api (7.0.0) multi_json elasticsearch-model (7.1.1) activesupport (> 3) elasticsearch (> 1) hashie elasticsearch-rails (7.1.0) - elasticsearch-transport (6.8.3) - faraday (~> 1) + elasticsearch-transport (7.0.0) + faraday multi_json erubi (1.12.0) erubis (2.7.0) - excon (0.93.1) + excon (0.109.0) execjs (2.8.1) - factory_bot (6.4.2) + factory_bot (6.4.5) activesupport (>= 5.0.0) - factory_bot_rails (6.4.2) + factory_bot_rails (6.4.3) factory_bot (~> 6.4) railties (>= 5.0.0) - faker (3.2.2) + faker (3.2.3) i18n (>= 1.8.11, < 2) - faraday (1.10.3) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.0) - faraday (~> 1.0) - ffi (1.15.5) + faraday (2.9.0) + faraday-net_http (>= 2.0, < 3.2) + faraday-net_http (3.1.0) + net-http + ffi (1.16.3) flickraw (0.9.10) font-awesome-sass (5.15.1) sassc (>= 1.11) friendly_id (5.5.1) activerecord (>= 4.0.0) + gbifrb (0.2.0) geocoder (1.8.2) gibbon (1.2.1) httparty @@ -263,8 +283,9 @@ GEM gravatar-ultimate (2.0.0) activesupport (>= 2.3.14) rack - haml (5.2.2) - temple (>= 0.8.0) + haml (6.3.0) + temple (>= 0.8.2) + thor tilt haml-i18n-extractor (0.5.9) activesupport @@ -277,8 +298,8 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.52.0) - haml (>= 4.0) + haml_lint (0.55.0) + haml (>= 5.0) parallel (~> 1.10) rainbow rubocop (>= 1.0) @@ -307,15 +328,21 @@ GEM rails-i18n rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) + icalendar (2.10.1) + ice_cube (~> 0.16) + ice_cube (0.16.4) + ice_nine (0.11.2) image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) + io-console (0.7.2) + irb (1.11.1) + rdoc + reline (>= 0.4.2) jquery-rails (4.6.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-ui-rails (6.0.1) - railties (>= 3.2.16) json (2.7.1) json-schema (4.1.1) addressable (>= 2.8) @@ -364,25 +391,28 @@ GEM mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.20.0) + minitest (5.21.2) moneta (1.0.0) + msgpack (1.7.2) multi_json (1.15.0) multi_xml (0.6.0) - multipart-post (2.2.3) - net-imap (0.3.7) + mutex_m (0.2.0) + net-http (0.4.1) + uri + net-imap (0.4.9.1) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout - net-smtp (0.3.3) + net-smtp (0.4.0.1) net-protocol - nio4r (2.5.9) - nokogiri (1.15.5) + nio4r (2.7.0) + nokogiri (1.16.0) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.15.5-x86_64-linux) + nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) oauth (0.5.6) oj (3.16.3) @@ -401,43 +431,54 @@ GEM rack orm_adapter (0.5.0) parallel (1.24.0) - parser (3.2.2.4) + parser (3.3.0.5) ast (~> 2.4.1) racc percy-capybara (5.0.0) capybara (>= 3) pg (1.5.4) - platform-api (3.5.0) + platform-api (3.6.0) heroics (~> 0.1.1) moneta (~> 1.0.0) rate_throttle_client (~> 0.1.0) popper_js (1.16.1) + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) + psych (5.1.2) + stringio public_suffix (5.0.4) - puma (6.4.0) + puma (6.4.2) nio4r (~> 2.0) query_diet (0.7.1) racc (1.7.3) rack (2.2.8) rack-cors (2.0.1) rack (>= 2.0.0) - rack-protection (3.1.0) + rack-protection (3.2.0) + base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) + rack-session (1.0.2) + rack (< 3) rack-test (2.1.0) rack (>= 1.3) - rails (7.0.8) - actioncable (= 7.0.8) - actionmailbox (= 7.0.8) - actionmailer (= 7.0.8) - actionpack (= 7.0.8) - actiontext (= 7.0.8) - actionview (= 7.0.8) - activejob (= 7.0.8) - activemodel (= 7.0.8) - activerecord (= 7.0.8) - activestorage (= 7.0.8) - activesupport (= 7.0.8) + rackup (1.0.0) + rack (< 3) + webrick + rails (7.1.3) + actioncable (= 7.1.3) + actionmailbox (= 7.1.3) + actionmailer (= 7.1.3) + actionpack (= 7.1.3) + actiontext (= 7.1.3) + actionview (= 7.1.3) + activejob (= 7.1.3) + activemodel (= 7.1.3) + activerecord (= 7.1.3) + activestorage (= 7.1.3) + activesupport (= 7.1.3) bundler (>= 1.15.0) - railties (= 7.0.8) + railties (= 7.1.3) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -457,13 +498,14 @@ GEM rails_stdout_logging rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (7.0.8) - actionpack (= 7.0.8) - activesupport (= 7.0.8) - method_source + railties (7.1.3) + actionpack (= 7.1.3) + activesupport (= 7.1.3) + irb + rackup (>= 1.0.0) rake (>= 12.2) - thor (~> 1.0) - zeitwerk (~> 2.5) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) rainbow (3.1.1) raindrops (0.20.0) rake (13.1.0) @@ -471,9 +513,14 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.10.1) ffi (~> 1.0) - redis-client (0.18.0) + rdoc (6.6.2) + psych (>= 4.0.0) + recaptcha (5.16.0) + redis-client (0.19.1) connection_pool - regexp_parser (2.8.3) + regexp_parser (2.9.0) + reline (0.4.2) + io-console (~> 0.5) responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) @@ -495,7 +542,7 @@ GEM rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.1.0) + rspec-rails (6.1.1) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -518,11 +565,11 @@ GEM rswag-ui (2.13.0) actionpack (>= 3.1, < 7.2) railties (>= 3.1, < 7.2) - rubocop (1.59.0) + rubocop (1.60.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.2.4) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) @@ -531,22 +578,22 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.30.0) parser (>= 3.2.1.0) - rubocop-capybara (2.19.0) + rubocop-capybara (2.20.0) rubocop (~> 1.41) - rubocop-factory_bot (2.24.0) - rubocop (~> 1.33) - rubocop-rails (2.23.0) + rubocop-factory_bot (2.25.1) + rubocop (~> 1.41) + rubocop-rails (2.23.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) rubocop-ast (>= 1.30.0, < 2.0) - rubocop-rspec (2.25.0) + rubocop-rspec (2.26.1) rubocop (~> 1.40) rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) ruby-progressbar (1.13.0) ruby-units (4.0.1) - ruby-vips (2.1.4) + ruby-vips (2.2.0) ffi (~> 1.12) ruby2_keywords (0.0.5) rubyzip (2.3.2) @@ -565,19 +612,19 @@ GEM tilt scout_apm (5.3.5) parser - searchkick (4.6.3) - activemodel (>= 5) - elasticsearch (>= 6, < 7.14) + searchkick (5.3.1) + activemodel (>= 6.1) hashie - selenium-webdriver (4.10.0) + selenium-webdriver (4.17.0) + base64 (~> 0.2) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sidekiq (7.2.0) + sidekiq (7.2.1) concurrent-ruby (< 2) connection_pool (>= 2.3.0) rack (>= 2.2.4) - redis-client (>= 0.14.0) + redis-client (>= 0.19.0) simplecov (0.13.0) docile (~> 1.1.0) json (>= 1.8, < 3) @@ -591,40 +638,44 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - ssrf_filter (1.0.7) + ssrf_filter (1.1.2) + stringio (3.1.0) sysexits (1.2.0) temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) + terser (1.2.0) + execjs (>= 0.3.0, < 3) thor (1.3.0) + thread_safe (0.3.6) tilt (2.3.0) timecop (0.9.8) - timeout (0.4.0) + timeout (0.4.1) trollop (1.16.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - uglifier (4.2.0) - execjs (>= 0.3.0, < 3) unicode-display_width (2.5.0) unicorn (6.1.0) kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.16.0) + uri (0.13.0) validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix + vcr (6.2.0) + virtus (2.0.0) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) warden (1.2.9) rack (>= 2.0.9) - webdrivers (5.3.1) - nokogiri (~> 1.6) - rubyzip (>= 1.3.0) - selenium-webdriver (~> 4.0, < 4.11) webrat (0.7.3) nokogiri (>= 1.2.0) rack (>= 1.0) rack-test (>= 0.5.3) webrick (1.8.1) - websocket (1.2.9) + websocket (1.2.10) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -642,9 +693,11 @@ PLATFORMS x86_64-linux DEPENDENCIES - active_median (= 0.2.0) + active_median active_record_union active_utils + axe-core-capybara + axe-core-rspec better_errors bluecloth bonsai-elasticsearch-rails @@ -668,14 +721,14 @@ DEPENDENCIES devise discard (>= 1.2) dotenv-rails - elasticsearch (< 7.0.0) + elasticsearch (~> 7.0.0) factory_bot_rails faker faraday - faraday_middleware flickraw font-awesome-sass friendly_id + gbifrb geocoder gibbon (~> 1.2.0) gravatar-ultimate @@ -685,8 +738,9 @@ DEPENDENCIES haml_lint (>= 0.25.1) hashie (>= 3.5.3) i18n-tasks + icalendar jquery-rails - jquery-ui-rails + jquery-ui-rails! jsonapi-resources jsonapi-swagger leaflet-rails (>= 1.9.2) @@ -697,6 +751,7 @@ DEPENDENCIES material-sass (= 4.1.1) material_icons memcachier + msgpack oj omniauth (~> 1.3) omniauth-flickr (>= 0.0.15) @@ -704,15 +759,17 @@ DEPENDENCIES percy-capybara (~> 5.0.0) pg platform-api + pry puma query_diet rack-cors rack-protection (>= 2.0.1) - rails (~> 7.0.7) + rails (~> 7.1.0) rails-assets-leaflet.markercluster! rails-controller-testing rails_12factor rake (>= 10.0.0) + recaptcha responders rspec-activemodel-mocks rspec-rails @@ -730,18 +787,18 @@ DEPENDENCIES selenium-webdriver sidekiq sprockets (< 4) + terser timecop - uglifier unicorn validate_url - webdrivers + vcr webrat will_paginate will_paginate-bootstrap-style xmlrpc RUBY VERSION - ruby 3.1.4p223 + ruby 3.1.4p223 BUNDLED WITH 2.3.11 diff --git a/app/assets/javascripts/crops.js.coffee b/app/assets/javascripts/crops.js.coffee index 5e96b90e0..cefbc609d 100644 --- a/app/assets/javascripts/crops.js.coffee +++ b/app/assets/javascripts/crops.js.coffee @@ -5,7 +5,7 @@ jQuery -> $(".remove-altname-row").css("display", "inline-block") -$ -> - sci_template = "
Scientific name of crop.
" + sci_template = "
Scientific name of crop
" sci_index = $('#scientific_names .template').length + 1 @@ -21,7 +21,7 @@ jQuery -> element = document.getElementById(tmp) element.remove() - alt_template = "
Alternate name of crop.
" + alt_template = "
Alternate name of crop.
" alt_index = $('#alternate_names .template').length + 1 diff --git a/app/assets/javascripts/scientific_name_auto_suggest.js.coffee b/app/assets/javascripts/scientific_name_auto_suggest.js.coffee new file mode 100644 index 000000000..f46ef37d1 --- /dev/null +++ b/app/assets/javascripts/scientific_name_auto_suggest.js.coffee @@ -0,0 +1,31 @@ +# TODO: This assumes one autocomplete per page. +# Needs to be a function so that when we append one of these, it gets uniquely associated with the hidden controls. +jQuery -> + + if el = $( '.scientific-name-auto-suggest' ) + + id = $( '.scientific-name-auto-suggest-id' ) + + el.autocomplete + minLength: 3, + source: el.attr( 'data-source-url' ), + focus: ( event, ui ) -> + el.val( ui.item.canonicalName ) + id.val( ui.item.nameKey ) + false + select: ( event, ui ) -> + el.val( ui.item.canonicalName ) + id.val( ui.item.nameKey ) + false + response: ( event, ui ) -> + id.val( "" ) + for item in ui.content + if item.name == el.val() + id.val( item.nameKey ) + + if el.data( 'uiAutocomplete' ) + el.data( 'uiAutocomplete' )._renderItem = ( ul, item ) -> + $( '
  • ' ) + .data( 'item.autocomplete', item ) + .append( "#{item.canonicalName} (#{item.scientificName}) - #{item.rank}" ) + .appendTo( ul ) diff --git a/app/assets/stylesheets/_crops.scss b/app/assets/stylesheets/_crops.scss index 26fd86e8b..cbbdf41b7 100644 --- a/app/assets/stylesheets/_crops.scss +++ b/app/assets/stylesheets/_crops.scss @@ -19,7 +19,7 @@ padding-bottom: 0; } - h5.crop-sci-name { + .crop-sci-name { background-color: $beige; color: $black; font-size: 0.7em; diff --git a/app/assets/stylesheets/_variables.scss b/app/assets/stylesheets/_variables.scss index 3e1848e19..c15007b54 100644 --- a/app/assets/stylesheets/_variables.scss +++ b/app/assets/stylesheets/_variables.scss @@ -1,10 +1,10 @@ //$screen-md-min: 1028px // Base colours -$beige: #f3f1ee; +$beige: #f4f2ef; $brown: #413f3b; -$green: #5f8e43; +$green: #57803c; $blue: #2f4365; $red: #ff4d43; $orange: #ffa500; diff --git a/app/assets/stylesheets/overrides.scss b/app/assets/stylesheets/overrides.scss index 07da5fb06..90f32de1f 100755 --- a/app/assets/stylesheets/overrides.scss +++ b/app/assets/stylesheets/overrides.scss @@ -101,12 +101,16 @@ section { background: $white; box-shadow: 1px 3px 3px 1px darken($beige, 20%); cursor: pointer; - transition: 0.3s transform cubic-bezier(0.155, 1.105, 0.295, 1.12), 0.3s box-shadow, + transition: + 0.3s transform cubic-bezier(0.155, 1.105, 0.295, 1.12), + 0.3s box-shadow, 0.3s -webkit-transform cubic-bezier(0.155, 1.105, 0.295, 1.12); } .card:hover { - box-shadow: 0 10px 20px darken($beige, 30%), 0 4px 8px darken($beige, 40%); + box-shadow: + 0 10px 20px darken($beige, 30%), + 0 4px 8px darken($beige, 40%); transform: scale(1.1); } } @@ -354,7 +358,9 @@ ul.thumbnail-buttons { text-align: center; } } - +.text-muted { + color: $blue; +} .jumbotron { background-color: $beige; margin-bottom: 1em; @@ -364,15 +370,23 @@ ul.thumbnail-buttons { h1 { font-size: 400%; } + .stats a { + color: $black; + } // signup widget on homepage .signup { - background-color: lighten($green, 40%); - border: 1px solid lighten($green, 20%); + background-color: darken($green, 20%); + border: 1px solid darken($green, 20%); + color: $white; border-radius: 6px; line-height: 200%; padding: 15px; text-align: center; + + .btn-info { + background-color: $blue; + } } .info { diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index 3ad46e6ac..587db6b33 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -8,7 +8,7 @@ module Admin responders :flash def index - @roles = Role.all.order(:name) + @roles = Role.all.order(:name).paginate(page: params[:page]) respond_with @roles end @@ -33,7 +33,7 @@ module Admin def destroy @role.destroy - respond_with @role, location: admin_roles_path + respond_with @role, location: admin_roles_path, notice: "Role was successfully deleted" end private diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 454faf1c7..166c06c0f 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -8,7 +8,7 @@ class AdminController < ApplicationController def newsletter authorize! :manage, :all - @members = Member.confirmed.wants_newsletter.all + @members = Member.confirmed.wants_newsletter.all.paginate(page: params[:page], per_page: 100) respond_with @members end end diff --git a/app/controllers/alternate_names_controller.rb b/app/controllers/alternate_names_controller.rb index 922c078cf..4594dddf0 100644 --- a/app/controllers/alternate_names_controller.rb +++ b/app/controllers/alternate_names_controller.rb @@ -9,7 +9,7 @@ class AlternateNamesController < ApplicationController # GET /alternate_names # GET /alternate_names.json def index - @alternate_names = AlternateName.all.order(:name) + @alternate_names = AlternateName.all.order(:name).paginate(page: params[:page], per_page: 100) respond_with(@alternate_names) end diff --git a/app/controllers/crops_controller.rb b/app/controllers/crops_controller.rb index b927f53ee..1332967a1 100644 --- a/app/controllers/crops_controller.rb +++ b/app/controllers/crops_controller.rb @@ -46,6 +46,12 @@ class CropsController < ApplicationController respond_with @crop, location: @crop end + def gbif + @crop = Crop.find(params[:crop_slug]) + @crop.update_gbif_data! + respond_with @crop, location: @crop + end + def hierarchy @crops = Crop.toplevel.order(:name) respond_with @crops @@ -57,7 +63,7 @@ class CropsController < ApplicationController @crops = CropSearchService.search(@term, page: params[:page], per_page: Crop.per_page, - current_member:) + current_member:).to_a respond_with @crops end @@ -120,6 +126,7 @@ class CropsController < ApplicationController if @crop.approval_status_changed?(from: "pending", to: "approved") notifier.deliver_now! @crop.update_openfarm_data! + @crop.update_gbif_data! end else @crop.approval_status = @crop.approval_status_was @@ -163,7 +170,7 @@ class CropsController < ApplicationController return if params[param_name].blank? destroy_names(name_type) - params[param_name].each do |_i, value| + params[param_name].each_value do |value| create_name!(name_type, value) unless value.empty? end end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index f01ad3d71..e19114b88 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -41,7 +41,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def after_sign_in_path_for(resource) if resource.tos_agreement - super resource + super(resource) else finish_signup_path(resource) end diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index 08df6bf70..91dd554c6 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -63,7 +63,7 @@ class PhotosController < ApplicationController def photo_params params.require(:photo).permit(:source_id, :source, :title, :license_name, - :license_url, :thumbnail_url, :fullsize_url, :link_url) + :license_url, :thumbnail_url, :fullsize_url, :link_url, :date_taken) end # Item with photos attached diff --git a/app/controllers/plant_parts_controller.rb b/app/controllers/plant_parts_controller.rb index 8fbd23c59..582f5d75d 100644 --- a/app/controllers/plant_parts_controller.rb +++ b/app/controllers/plant_parts_controller.rb @@ -6,7 +6,7 @@ class PlantPartsController < ApplicationController responders :flash def index - @plant_parts = PlantPart.all.order(:name) + @plant_parts = PlantPart.all.order(:name).paginate(page: params[:page], per_page: 100) respond_with(@plant_parts) end diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index 6c02f5128..73a0538f2 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -3,6 +3,7 @@ class PlantingsController < DataController after_action :update_crop_medians, only: %i(create update destroy) after_action :update_planting_medians, only: :update + respond_to :ics, only: [:index] # TODO: This can be shifted up when all relevant controllers respond to ical def index @show_all = params[:all] == '1' @@ -29,7 +30,6 @@ class PlantingsController < DataController ) @filename = "Growstuff-#{specifics}Plantings-#{Time.zone.now.to_fs(:number)}.csv" - respond_with(@plantings) end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 29cc5e1b1..862a18689 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -3,6 +3,8 @@ class RegistrationsController < Devise::RegistrationsController respond_to :json + prepend_before_action :check_captcha, only: [:create] # Change this to be any actions you want to protect with recaptcha. + def edit @twitter_auth = current_member.auth('twitter') @flickr_auth = current_member.auth('flickr') @@ -46,6 +48,25 @@ class RegistrationsController < Devise::RegistrationsController render "edit" end end + + private + + def sign_up_params + params.require(:member).permit(:login_name, :email, :tos_agreement, :newsletter, :password, :password_confirmation) + end + + def check_captcha + return if verify_recaptcha # verify_recaptcha(action: 'signup') for v3 + + self.resource = resource_class.new sign_up_params + resource.validate # Look for any other validation errors besides reCAPTCHA + set_minimum_password_length + + respond_with_navigational(resource) do + flash.discard(:recaptcha_error) # We need to discard flash to avoid showing it on the next page reload + render :new + end + end end # check if we need the current password to update fields diff --git a/app/controllers/scientific_names_controller.rb b/app/controllers/scientific_names_controller.rb index 94a18acf9..972e06f90 100644 --- a/app/controllers/scientific_names_controller.rb +++ b/app/controllers/scientific_names_controller.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true class ScientificNamesController < ApplicationController - before_action :authenticate_member!, except: %i(index show) - load_and_authorize_resource + before_action :authenticate_member!, except: %i(index show gbif_suggest) + load_and_authorize_resource except: [:gbif_suggest] respond_to :html, :json responders :flash # GET /scientific_names # GET /scientific_names.json def index - @scientific_names = ScientificName.all.order(:name) + @scientific_names = ScientificName.all.order(:name).paginate(page: params[:page], per_page: 100) respond_with(@scientific_names) end @@ -35,7 +35,7 @@ class ScientificNamesController < ApplicationController def create @scientific_name = ScientificName.new(scientific_name_params) @scientific_name.creator = current_member - + gbif_sync!(@scientific_name) @scientific_name.save respond_with(@scientific_name.crop) end @@ -43,7 +43,9 @@ class ScientificNamesController < ApplicationController # PUT /scientific_names/1 # PUT /scientific_names/1.json def update - @scientific_name.update(scientific_name_params) + @scientific_name.assign_attributes(scientific_name_params) + gbif_sync!(@scientific_name) + @scientific_name.save respond_with(@scientific_name.crop) end @@ -56,9 +58,26 @@ class ScientificNamesController < ApplicationController respond_with(@crop) end + def gbif_suggest + render json: gbif_service.suggest(params[:term]) + end + private + def gbif_sync!(model) + return unless model.gbif_key + + result = gbif_service.fetch(model.gbif_key) + + model.gbif_rank = result["rank"] + model.gbif_status = result["status"] + end + def scientific_name_params - params.require(:scientific_name).permit(:crop_id, :name) + params.require(:scientific_name).permit(:crop_id, :name, :gbif_key) + end + + def gbif_service + GbifService.new end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 511e2b352..61d44c90d 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -93,7 +93,7 @@ module IconsHelper def plant_part_icon(name) if File.exist? Rails.root.join('app', 'assets', 'images', 'icons', 'plant_parts', "#{name}.svg") - image_tag "icons/plant_parts/#{name}.svg", class: 'img img-icon', 'aria-hidden' => "true" + image_tag "icons/plant_parts/#{name}.svg", class: 'img img-icon', 'aria-hidden' => "true", alt: name else planting_icon end @@ -101,7 +101,7 @@ module IconsHelper def crop_icon(crop) if crop.svg_icon.present? - image_tag(crop_path(crop, format: 'svg'), class: 'crop-icon') + image_tag(crop_path(crop, format: 'svg'), class: 'crop-icon', alt: crop) elsif crop.parent.present? crop_icon(crop.parent) else @@ -123,6 +123,6 @@ module IconsHelper end def image_icon(icon) - image_tag "icons/#{icon}.svg", class: 'img img-icon', 'aria-hidden' => "true" + image_tag "icons/#{icon}.svg", class: 'img img-icon', 'aria-hidden' => "true", alt: icon end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 3156b21ba..fb251a417 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -79,6 +79,7 @@ class Ability can :manage, ScientificName can :manage, AlternateName can :openfarm, Crop + can :gbif, Crop end # any member can create a crop provisionally diff --git a/app/models/concerns/gbif_data.rb b/app/models/concerns/gbif_data.rb new file mode 100644 index 000000000..a9bc27fcb --- /dev/null +++ b/app/models/concerns/gbif_data.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module GbifData + extend ActiveSupport::Concern + + included do + def update_gbif_data! + GbifService.new.update_crop(self) + end + end +end diff --git a/app/models/crop.rb b/app/models/crop.rb index 020692387..1ab135dff 100644 --- a/app/models/crop.rb +++ b/app/models/crop.rb @@ -4,6 +4,7 @@ class Crop < ApplicationRecord extend FriendlyId include PhotoCapable include OpenFarmData + include GbifData include SearchCrops friendly_id :name, use: %i(slugged finders) diff --git a/app/models/photo.rb b/app/models/photo.rb index 03aa17e53..4b4331490 100644 --- a/app/models/photo.rb +++ b/app/models/photo.rb @@ -16,8 +16,13 @@ class Photo < ApplicationRecord Crop.distinct.joins(:photo_associations).where(photo_associations: { photo: self }) end - validates :fullsize_url, url: true - validates :thumbnail_url, url: true + validates :fullsize_url, url: true, presence: true + validates :thumbnail_url, url: true, presence: true + validates :link_url, url: true, presence: true + validates :owner, presence: true + validates :title, presence: true + validates :license_name, presence: true # Should assert this is one of CC-BY, CC-BY-NC, etc + validates :license_url, url: true, allow_blank: true # creates a relationship for each assignee type PHOTO_CAPABLE.each do |type| diff --git a/app/models/post.rb b/app/models/post.rb index 12773fbb9..e89b2be41 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -24,8 +24,11 @@ class Post < ApplicationRecord # # Validations validates :subject, presence: true, length: { maximum: 255 } + validates :body, presence: true def author_date_subject + return unless author + # slugs are created before created_at is set time = created_at || Time.zone.now "#{author.login_name} #{time.strftime('%Y%m%d')} #{subject}" diff --git a/app/services/gbif_service.rb b/app/services/gbif_service.rb new file mode 100644 index 000000000..669c6bdd4 --- /dev/null +++ b/app/services/gbif_service.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +require 'English' +class GbifService + def initialize + @cropbot = Member.find_by(login_name: 'cropbot') + @species = Gbif::Species + end + + def suggest(term) + # Query the GBIF name autocomplete and discover the scientific name. + # [ + # { + # "key": 2932942, + # "nameKey": 1970347, + # "kingdom": "Plantae", + # "phylum": "Tracheophyta", + # "order": "Solanales", + # "family": "Solanaceae", + # "genus": "Capsicum", + # "species": "Capsicum chinense", + # "kingdomKey": 6, + # "phylumKey": 7707728, + # "classKey": 220, + # "orderKey": 1176, + # "familyKey": 7717, + # "genusKey": 2932937, + # "speciesKey": 2932942, + # "parent": "Capsicum", + # "parentKey": 2932937, + # "nubKey": 2932942, + # "scientificName": "Capsicum chinense Jacq.", + # "canonicalName": "Capsicum chinense", + # "rank": "SPECIES", + # "status": "ACCEPTED", + # "synonym": false, + # "higherClassificationMap": { + # "6": "Plantae", + # "220": "Magnoliopsida", + # "1176": "Solanales", + # "7717": "Solanaceae", + # "2932937": "Capsicum", + # "7707728": "Tracheophyta" + # }, + # "class": "Magnoliopsida" + # }, + # { + # "key": 12079498, + # "nameKey": 81778754, + # "kingdom": "Plantae", + # "phylum": "Tracheophyta", + # "order": "Solanales", + # "family": "Solanaceae", + # "genus": "Capsicum", + # "species": "Capsicum chinense", + # "kingdomKey": 6, + # "phylumKey": 7707728, + # "classKey": 220, + # "orderKey": 1176, + # "familyKey": 7717, + # "genusKey": 2932937, + # "speciesKey": 2932942, + # "parent": "Capsicum", + # "parentKey": 2932937, + # "nubKey": 12079498, + # "scientificName": "Capsicum annuum var. chinense (Jacq.) Alef.", + # "canonicalName": "Capsicum annuum chinense", + # "rank": "VARIETY", + # "status": "SYNONYM", + # "synonym": true, + # "higherClassificationMap": { + # "6": "Plantae", + # "220": "Magnoliopsida", + # "1176": "Solanales", + # "7717": "Solanaceae", + # "2932937": "Capsicum", + # "2932942": "Capsicum chinense", + # "7707728": "Tracheophyta" + # }, + # "class": "Magnoliopsida" + # } + # ] + + @species.name_suggest(q: term) + end + + def import! + Crop.order(updated_at: :desc).each do |crop| + Rails.logger.debug { "#{crop.id}, #{crop.name}" } + update_crop(crop) if crop.valid? + rescue ActiveRecord::RecordInvalid + Rails.logger.error($ERROR_INFO.message) + end + end + + def update_crop(crop) + # Attempt to resolve the scientific names via /species/match. + gbif_usage_key = crop.scientific_names.detect { |sn| sn.gbif_key.present? }&.gbif_key + unless gbif_usage_key + crop.scientific_names.each do |sn| + result = @species.name_backbone(name: sn.name) # , higherTaxonKey: 6, nameType: 'SCIENTIFIC') + next unless result["confidence"] > 95 && result["matchType"] == "EXACT" + + sn.gbif_key = result["usageKey"] + sn.gbif_rank = result["rank"] + sn.gbif_status = result["status"] + sn.save! + end + + gbif_usage_key = crop.scientific_names.detect { |sn| sn.gbif_key.present? }&.gbif_key + end + + # No match? Fall back to common names + unless gbif_usage_key + query_results = @species.name_lookup(q: crop.name, higherTaxonKey: 6) + + # We only want one result, otherwise it needs human. + return unless query_results["results"].length == 1 + + query_result = query_results["results"].first + + gbif_usage_key = query_result["key"] + + crop.scientific_names.create!(gbif_key: gbif_usage_key, name: query_result["canonicalName"], creator: @cropbot) + end + + gbif_record = fetch(gbif_usage_key) + if gbif_record.present? + # crop.update! openfarm_data: gbif_record.fetch('data', false) + # save_companions(crop, gbif_record) + save_photos(crop, gbif_usage_key) + else + Rails.logger.debug "\tcrop not found on GBIF" + # crop.update!(openfarm_data: false) + end + end + + def save_photos(crop, key) + # https://api.gbif.org/v1/occurrence/search?taxon_key=3084850 + + occurrences = Gbif::Occurrences.search(taxonKey: key, mediatype: 'StillImage', limit: 3, hasCoordinate: true) + occurrences["results"].each do |result| + next unless result["media"] + + media = result["media"].first + next unless media["identifier"] + + # Example: "https://inaturalist-open-data.s3.amazonaws.com/photos/250226497/original.jpg" + url = media["identifier"] + md5 = Digest::MD5.hexdigest(url) + width = 200 + thumbnail = "https://api.gbif.org/v1/image/cache/#{width}x/occurrence/#{result['key']}/media/#{md5}" + + next unless url.start_with? 'http' + next if Photo.find_by(source_id: result["key"], source: 'gbif') + next if media["references"].blank? + + photo = Photo.new( + # This is for the overall observation which may technically have multiple media. However, we're only taking the first. + source_id: result["key"], + source: 'gbif', + owner: @cropbot, + thumbnail_url: thumbnail, + fullsize_url: url, + title: "Photo by #{media['creator']} via #{media['publisher']} (Copyright #{media['rightsHolder']})", + license_name: case media["license"] + when "http://creativecommons.org/licenses/by/4.0/" + "CC BY 4.0" + when "http://creativecommons.org/licenses/by-nc/4.0/" + "CC BY-NC 4.0" + else + media["license"] + end, + license_url: media["license"], + link_url: media["references"] + ) + photo.date_taken = DateTime.parse(media["created"]) if media["created"] + if photo.valid? + Photo.transaction do + photo.save + PhotoAssociation.find_or_create_by! photo:, photographable: crop + end + Rails.logger.debug { "\t saved photo #{photo.id} #{photo.source_id}" } + else + Rails.logger.warn "Photo not valid" + end + end + end + + def fetch(key) + Gbif::Request.new("species/#{key}", nil, nil, nil).perform + end +end diff --git a/app/views/admin/index.html.haml b/app/views/admin/index.html.haml index 9a60bb324..d987411df 100644 --- a/app/views/admin/index.html.haml +++ b/app/views/admin/index.html.haml @@ -30,6 +30,7 @@ %li= link_to "Crop Wrangling", wrangle_crops_path, class: 'nav-link' %li= link_to "Alternate names", alternate_names_path, class: 'nav-link' %li= link_to "Scientific names", scientific_names_path, class: 'nav-link' + %li= link_to "Plant parts", plant_parts_path, class: 'nav-link' .col-md-4 .card diff --git a/app/views/admin/newsletter.html.haml b/app/views/admin/newsletter.html.haml index 38f415329..f6ad57c1f 100644 --- a/app/views/admin/newsletter.html.haml +++ b/app/views/admin/newsletter.html.haml @@ -4,3 +4,4 @@ - @members.each do |m| = m.email %br/ += will_paginate @members diff --git a/app/views/admin/roles/index.html.haml b/app/views/admin/roles/index.html.haml index eced7a10d..587968f4d 100644 --- a/app/views/admin/roles/index.html.haml +++ b/app/views/admin/roles/index.html.haml @@ -5,7 +5,7 @@ %li.breadcrumb-item.active= link_to 'Roles', admin_roles_path - if can? :create, Role - %p= link_to 'New Role', new_admin_role_path, class: 'btn btn-primary' + %p= link_to 'New role', new_admin_role_path, class: 'btn btn-primary' %table.table.table-striped %thead @@ -25,6 +25,8 @@ = edit_icon = t('.edit') - if can?(:destroy, role) && ! role.members.any? - = link_to admin_role_path(role), method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-default btn-xs text-danger' do + = link_to admin_role_path(role), method: :delete, data: { confirm: t(:are_you_sure?) }, class: 'btn btn-default btn-xs text-danger' do = delete_icon = t('.delete') + += will_paginate(@roles) diff --git a/app/views/alternate_names/_form.html.haml b/app/views/alternate_names/_form.html.haml index 6c9bbf283..68f6a138a 100644 --- a/app/views/alternate_names/_form.html.haml +++ b/app/views/alternate_names/_form.html.haml @@ -3,7 +3,7 @@ - if content_for? :title %h1.h2-responsive.text-center %strong=yield :title - = form_for @alternate_name, html: { class: 'form-horizontal', role: "form" } do |f| + = form_for @alternate_name, html: { class: 'form-horizontal' } do |f| - if @alternate_name.errors.any? #error_explanation %h2 diff --git a/app/views/alternate_names/index.html.haml b/app/views/alternate_names/index.html.haml index 45b42b647..ac5da25a4 100644 --- a/app/views/alternate_names/index.html.haml +++ b/app/views/alternate_names/index.html.haml @@ -17,10 +17,11 @@ %td= link_to 'Show', alternate_name %td - if can? :edit, alternate_name - = link_to 'Edit', edit_alternate_name_path(alternate_name), class: 'btn btn-default btn-xs' + = link_to t('buttons.edit'), edit_alternate_name_path(alternate_name), class: 'btn btn-default btn-xs' %td - if can? :destroy, alternate_name - = link_to 'Delete', alternate_name, - method: :delete, - data: { confirm: 'Are you sure?' }, - class: 'btn btn-default btn-xs' + = link_to t('buttons.delete'), alternate_name, + method: :delete, + data: { confirm: t(:are_you_sure?) }, + class: 'btn btn-default btn-xs' += will_paginate(@alternate_names) diff --git a/app/views/comments/_form.html.haml b/app/views/comments/_form.html.haml index 43b9073b9..c8cff6eed 100644 --- a/app/views/comments/_form.html.haml +++ b/app/views/comments/_form.html.haml @@ -3,7 +3,7 @@ - if content_for? :title %h1.h2-responsive.text-center %strong=yield :title - = form_for(@comment, html: { class: "form-horizontal", role: "form" }) do |f| + = form_for(@comment, html: { class: "form-horizontal" }) do |f| - if @comment.errors.any? #error_explanation %h2 diff --git a/app/views/comments/index.rss.haml b/app/views/comments/index.rss.haml index 9c902face..fdfda46db 100644 --- a/app/views/comments/index.rss.haml +++ b/app/views/comments/index.rss.haml @@ -7,7 +7,7 @@ %item %title Comment by #{comment.author.login_name} on #{comment.post.subject} %description - :escaped + :escaped_markdown

    Comment on #{ link_to comment.post.subject, post_url(comment.post) } diff --git a/app/views/conversations/index.haml b/app/views/conversations/index.haml index 550ecf9b7..8fdc02bf7 100644 --- a/app/views/conversations/index.haml +++ b/app/views/conversations/index.haml @@ -52,9 +52,9 @@ = truncate(strip_tags(conversation.messages.last.body), length: 150, separator: ' ', omission: '... ') .col-md-1 - if @box == 'trash' - = link_to conversation_path(conversation, box: @box), method: :put, class: 'restore' do + = link_to conversation_path(conversation, box: @box), method: :put, class: 'restore', title: "Restore" do = icon 'fas', 'trash-restore' - else - = check_box_tag 'conversation_ids[]', conversation.id, false, class: 'selectable' + = check_box_tag 'conversation_ids[]', conversation.id, false, class: 'selectable', "aria-label": "Select for deletion" - unless @conversations.empty? = will_paginate @conversations diff --git a/app/views/crops/_crop.html.haml b/app/views/crops/_crop.html.haml index f5c8ae1e3..8bc2fb282 100644 --- a/app/views/crops/_crop.html.haml +++ b/app/views/crops/_crop.html.haml @@ -1,7 +1,7 @@ .card.card-crop .crop-image = link_to image_tag(crop_image_path(crop), - alt: '', + alt: "Image of #{crop.name}", class: 'img img-card'), crop .card-body diff --git a/app/views/crops/_form.html.haml b/app/views/crops/_form.html.haml index dcf451e6a..e3ea3b344 100644 --- a/app/views/crops/_form.html.haml +++ b/app/views/crops/_form.html.haml @@ -65,8 +65,12 @@ .col-2 = label_tag :scientific_names, "Scientific name #{index + 1}:", class: 'control-label' .col-8 - = text_field_tag "sci_name[#{index + 1}]", sci.name, id: "sci_name[#{index + 1}]", class: 'form-control' - %span.help-block Scientific name of crop. + = text_field_tag "sci_name[#{index + 1}]", sci.name, id: "sci_name[#{index + 1}]", + class: 'scientific-name-auto-suggest form-control', + data: { source_url: gbif_suggest_scientific_names_path } + %span.help-block Searches GBIF to determine scientific name of crop. + = hidden_field_tag "sci_gbif_key[#{index + 1}]", sci.gbif_key, id: "sci_gbif_key[#{index + 1}]", + class: 'scientific-name-auto-suggest-id' %h2 Alternate names = button_tag "+", class: "add-altname-row", type: "button" = button_tag "-", class: "remove-altname-row", type: "button" diff --git a/app/views/crops/_image_with_popover.html.haml b/app/views/crops/_image_with_popover.html.haml index a8ce256bd..6176c6bba 100644 --- a/app/views/crops/_image_with_popover.html.haml +++ b/app/views/crops/_image_with_popover.html.haml @@ -1,6 +1,6 @@ - cache crop do = link_to image_tag(crop_image_path(crop), - alt: crop.name, class: 'image-responsive crop-image'), + alt: "Image of #{crop.name}", class: 'image-responsive crop-image'), crop.name, rel: "popover", 'data-trigger': 'hover', diff --git a/app/views/crops/_info.haml b/app/views/crops/_info.haml index adc610461..3b1c34368 100644 --- a/app/views/crops/_info.haml +++ b/app/views/crops/_info.haml @@ -25,4 +25,4 @@ %p= simple_format @crop.description .col-md-3 = image_tag crop_image_path(@crop), - class: 'img-responsive shadow rounded crop-hero-photo', alt: 'photo of crop' + class: 'img-responsive shadow rounded crop-hero-photo', alt: "Image of #{@crop.name}" diff --git a/app/views/crops/_scientific_names.html.haml b/app/views/crops/_scientific_names.html.haml index bf6ace250..fcdfd4c3f 100644 --- a/app/views/crops/_scientific_names.html.haml +++ b/app/views/crops/_scientific_names.html.haml @@ -16,7 +16,13 @@ = delete_icon = t('.delete') - else - .badge= sn.name + - if sn.gbif_key + = link_to sn.name, "https://www.gbif.org/species/#{sn.gbif_key}", + class: 'card-link', + target: "_blank", + rel: "noopener noreferrer" + - else + .badge= sn.name %p.text-right - if can? :edit, crop diff --git a/app/views/crops/_thumbnail.html.haml b/app/views/crops/_thumbnail.html.haml index 6500d913d..0566ada95 100644 --- a/app/views/crops/_thumbnail.html.haml +++ b/app/views/crops/_thumbnail.html.haml @@ -2,11 +2,12 @@ .card.crop-thumbnail = link_to crop_path(slug: crop.slug) do = image_tag(crop.thumbnail_url.presence || placeholder_image, - alt: crop.name, + alt: "Image of #{crop.name}", class: 'img img-card') .text %h3.crop-name= link_to crop.name, crop_path(slug: crop.slug) - %h5.crop-sci-name - = crop.scientific_names.first + - if crop.scientific_names.any? + %div.crop-sci-name + = crop.scientific_names.first diff --git a/app/views/crops/_wrangle.html.haml b/app/views/crops/_wrangle.html.haml index 6c93c734d..6129d46c4 100644 --- a/app/views/crops/_wrangle.html.haml +++ b/app/views/crops/_wrangle.html.haml @@ -14,6 +14,10 @@ = icon 'far', 'update' Fetch data from OpenFarm + = link_to crop_gbif_path(crop), method: :post, class: 'dropdown-item' do + = icon 'far', 'update' + Fetch data from GBIF + - if can? :destroy, crop .dropdown-divider = delete_button(crop, classes: 'dropdown-item text-danger') diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index 7053eb80e..9084bdbef 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -86,7 +86,7 @@ = link_to crop_plantings_path(@crop), class: 'card-link' do = planting_icon #{@crop.name.capitalize} plantings - %span.badge.badge-primary.badge-pill=@crop.plantings.size + %span.badge.badge-primary.badge-pill=@crop.plantings.active.size .list-group-item.d-flex.justify-content-between.align-items-center = link_to crop_harvests_path(@crop), class: 'card-link' do = harvest_icon diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml index dd8f93080..3a895b73f 100644 --- a/app/views/devise/registrations/new.html.haml +++ b/app/views/devise/registrations/new.html.haml @@ -3,9 +3,10 @@ %h1 Join #{ENV['GROWSTUFF_SITE_NAME']} .card-body %p Sign up for a #{ENV['GROWSTUFF_SITE_NAME']} account to track your vegetable garden and connect with other local growers. + %p If you have accessibility issues with the captcha, please contact us via the links in the footer and we will help. = bootstrap_form_for(resource, as: resource_name, url: registration_path(resource_name), - html: { class: "text-center border border-light p-5" }) do |f| + html: { class: "text-center border border-light p-5", data: { turbo: false } }) do |f| = render 'devise/shared/error_messages', resource: resource = f.text_field :login_name @@ -28,4 +29,9 @@ = f.submit "Sign up", class: 'btn btn-block btn-success' + -# START add reCAPTCHA + = flash[:recaptcha_error] + = recaptcha_tags + -# END add reCAPTCHA + .card-footer= render "devise/shared/links" diff --git a/app/views/garden_types/_form.html.haml b/app/views/garden_types/_form.html.haml index a6d84885a..fa107ed40 100644 --- a/app/views/garden_types/_form.html.haml +++ b/app/views/garden_types/_form.html.haml @@ -1,4 +1,4 @@ -= form_for @garden_type, html: { class: 'form-horizontal', role: "form" } do |f| += form_for @garden_type, html: { class: 'form-horizontal' } do |f| - if @garden_type.errors.any? #error_explanation %h2= "#{pluralize(@garden_type.errors.count, "error")} prohibited this garden_type from being saved:" diff --git a/app/views/gardens/_garden.html.haml b/app/views/gardens/_garden.html.haml index bba2a0ee4..6fd29cf50 100644 --- a/app/views/gardens/_garden.html.haml +++ b/app/views/gardens/_garden.html.haml @@ -1,5 +1,5 @@ .card = link_to garden do - = image_tag garden_image_path(garden), class: 'img-card', alt: garden + = image_tag garden_image_path(garden), class: 'img-card', alt: "Image of #{garden.name}" .card-body.text-center %h4.card-title= garden.name \ No newline at end of file diff --git a/app/views/gardens/_photo.html.haml b/app/views/gardens/_photo.html.haml index b6d301d2e..61afaeaf7 100644 --- a/app/views/gardens/_photo.html.haml +++ b/app/views/gardens/_photo.html.haml @@ -1,3 +1,3 @@ = link_to image_tag(garden_image_path(garden), - alt: garden.name, class: 'img-responsive'), + alt: "Image of #{garden.name}", class: 'img-responsive'), garden_path(garden) diff --git a/app/views/gardens/index.html.haml b/app/views/gardens/index.html.haml index 51a089f28..4bc4307e1 100644 --- a/app/views/gardens/index.html.haml +++ b/app/views/gardens/index.html.haml @@ -12,9 +12,10 @@ .row .col-md-2 = render 'layouts/nav', model: Garden - = link_to show_inactive_tickbox_path('gardens', owner: @owner, show_all: @show_all) do - = check_box_tag 'active', 'all', @show_all - include in-active + %label + = link_to show_inactive_tickbox_path('gardens', owner: @owner, show_all: @show_all) do + = check_box_tag 'active', 'all', @show_all + include in-active - if @owner.present? %hr/ = render @owner diff --git a/app/views/harvests/show.html.haml b/app/views/harvests/show.html.haml index 402002040..bccde79fa 100644 --- a/app/views/harvests/show.html.haml +++ b/app/views/harvests/show.html.haml @@ -33,7 +33,7 @@ %h3 Harvested = editable :date, @harvest, :harvested_at, display_field: '.harvested_at' - %strong.harvested_at #{time_ago_in_words @harvest.harvested_at} ago + %strong.harvested_at #{distance_of_time_in_words @harvest.harvested_at, Time.zone.now.to_date} ago %span.harvested_at= I18n.l @harvest.harvested_at .card{class: @harvest.quantity.present? ? '' : 'text-muted'} diff --git a/app/views/home/_blurb.html.haml b/app/views/home/_blurb.html.haml index 06cb8533c..f9997a028 100644 --- a/app/views/home/_blurb.html.haml +++ b/app/views/home/_blurb.html.haml @@ -1,9 +1,10 @@ .row.homepage-blurb .col-md-8.info + %h1 Growstuff - An open gardening platform %p= t('.intro', site_name: ENV['GROWSTUFF_SITE_NAME']) = render 'stats' .col-md-4 .signup %p= t('.perks') - %p= link_to(t('.sign_up'), new_member_registration_path, class: 'btn btn-info btn-block') + %p= link_to(t('.sign_up'), new_member_registration_path, class: 'btn btn-primary btn-block') %p= t('.already_html', sign_in: link_to(t('.sign_in_linktext'), new_member_session_path, class: 'btn btn-primary')) diff --git a/app/views/home/_harvests.html.haml b/app/views/home/_harvests.html.haml index 6e25c0a4c..0621e99a5 100644 --- a/app/views/home/_harvests.html.haml +++ b/app/views/home/_harvests.html.haml @@ -4,9 +4,9 @@ = link_to harvest_path(slug: harvest.slug), class: 'list-group-item list-group-item-action flex-column align-items-start' do .d-flex.w-100.justify-content-between.homepage--list-item %div - %h5= harvest.crop_name + %h4= harvest.crop_name %span.badge.badge-success=harvest.plant_part %small.text-muted harvested by #{harvest.owner_login_name} %p.mb-2 - = image_tag harvest.thumbnail_url, width: 75, class: 'rounded shadow' + = image_tag harvest.thumbnail_url, width: 75, class: 'rounded shadow', alt: "Image of #{harvest.crop_name} by #{harvest.owner}" diff --git a/app/views/home/_plantings.html.haml b/app/views/home/_plantings.html.haml index 797391927..af2d4fea8 100644 --- a/app/views/home/_plantings.html.haml +++ b/app/views/home/_plantings.html.haml @@ -3,9 +3,9 @@ = link_to planting_path(slug: planting['slug']), class: 'list-group-item list-group-item-action flex-column align-items-start' do .d-flex.w-100.justify-content-between.homepage--list-item %p.mb-2 - = image_tag planting['thumbnail_url'], width: 75, class: 'rounded shadow' + = image_tag planting['thumbnail_url'], width: 75, class: 'rounded shadow', alt: "Image of #{planting['crop_name']} by #{planting['owner_login_name']}" .text-right - %h5= planting['crop_name'] + %h4= planting['crop_name'] - if planting['planted_from'].present? %span.badge.badge-success= planting['planted_from'].pluralize %small.text-muted planted by #{planting['owner_login_name']} diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index f2f2d132c..adc174fc8 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -22,10 +22,10 @@ = render 'crops' = link_to "#{t('home.crops.view_all')} ยป", crops_path, class: 'btn btn-block' .col-xl-3.col-12 - %section.recent-crops + %section.recent-crops.card - cache cache_key_for(Crop, 'recent') do %h2= t('.recently_added') - %p + %p.card-body != CropSearchService.recent(30).map { |c| link_to(c['name'], crop_path(slug: c['slug'])) }.join(", ") .col-xl-3.col %section.plantings diff --git a/app/views/layouts/_menu.haml b/app/views/layouts/_menu.haml index fba2e4aea..3db3f3bee 100644 --- a/app/views/layouts/_menu.haml +++ b/app/views/layouts/_menu.haml @@ -3,13 +3,13 @@ - if signed_in? %li.nav-item = link_to timeline_index_path, method: :get, class: 'nav-link text-white' do - = image_tag 'icons/notification.svg', class: 'img img-icon' + = image_tag 'icons/notification.svg', class: 'img img-icon', alt: "Notifications" %li.nav-item - = link_to member_gardens_path(current_member), class: 'nav-link text-white' do + = link_to member_gardens_path(current_member), class: 'nav-link text-white', title: "My gardens" do = image_icon 'gardens' %li.nav-item.dropdown %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"} - = image_tag "icons/gardener.svg", class: 'img img-icon' + = image_tag "icons/gardener.svg", class: 'img img-icon', alt: t('.record'), aria: { hidden: "true" } = t('.record') .dropdown-menu = link_to new_planting_path, class: 'dropdown-item' do @@ -60,7 +60,7 @@ %li.nav-item.dropdown %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"} - = image_tag(avatar_uri(current_member, 50), alt: '', height: 25, width: 25) + = image_tag(avatar_uri(current_member, 50), alt: 'Avatar of current member', height: 25, width: 25, aria: { hidden: "true" }) = current_member.login_name - if current_member.unread_count.positive? %span.badge.badge-info= current_member.unread_count diff --git a/app/views/layouts/_nav.haml b/app/views/layouts/_nav.haml index 158f9043d..d7e287d85 100644 --- a/app/views/layouts/_nav.haml +++ b/app/views/layouts/_nav.haml @@ -1,9 +1,9 @@ - if current_member.present? - .flex-column.nav-pills.layout-nav + .flex-column.nav-pills.layout-nav{"role" => "tablist", "aria-orientation"=>"vertical"} / %h2.card-title #{model} links - = link_to url_for([current_member, model]), class: 'nav-link' do + = link_to url_for([current_member, model]), class: 'nav-link tab' do My #{model.model_name.human.pluralize} - = link_to model, class: 'nav-link' do + = link_to model, class: 'nav-link tab' do Everyone's #{model.model_name.human.pluralize} - if can?(:create, model) = link_to url_for([model, action: :new]), class: 'btn' do diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 87a62e934..311a712f7 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -12,9 +12,10 @@ .container #maincontainer - if content_for?(:breadcrumbs) - %ol.breadcrumb{ "aria-label" => "breadcrumb" } - %li.breadcrumb-item= link_to 'Home', root_path - = yield(:breadcrumbs) + %nav{ "aria-label" => "breadcrumb" } + %ol.breadcrumb + %li.breadcrumb-item= link_to 'Home', root_path + = yield(:breadcrumbs) - if content_for?(:buttonbar) = yield(:buttonbar) @@ -22,7 +23,7 @@ %small= yield(:subtitle) = render "shared/flash_messages", flash: flash - = yield + %main= yield %footer.page-footer.font-small.bg-dark.pt-4= render "layouts/footer" / diff --git a/app/views/members/_avatar.html.haml b/app/views/members/_avatar.html.haml index cc9a208c4..b4ea132a9 100644 --- a/app/views/members/_avatar.html.haml +++ b/app/views/members/_avatar.html.haml @@ -1 +1 @@ -= link_to image_tag(avatar_uri(member, 150), alt: member, class: 'avatar img img-fluid'), member_path(member) += link_to image_tag(avatar_uri(member, 150), alt: "Avatar of #{member}", class: 'avatar img img-fluid'), member_path(member) diff --git a/app/views/members/_tiny.haml b/app/views/members/_tiny.haml index 95b51c2b0..5522a8e70 100644 --- a/app/views/members/_tiny.haml +++ b/app/views/members/_tiny.haml @@ -4,7 +4,7 @@ = member - else = link_to member do - = image_tag(avatar_uri(member, 100), alt: '', height: 50, width: 50) + = image_tag(avatar_uri(member, 100), alt: member.login_name, height: 50, width: 50) = member diff --git a/app/views/members/show.html.haml b/app/views/members/show.html.haml index 2a62abac9..d880f2e47 100644 --- a/app/views/members/show.html.haml +++ b/app/views/members/show.html.haml @@ -74,6 +74,9 @@ = render 'timeline/photos', photo: resolve_model(event) if event.event_type == 'photo' %small - if event.event_at.present? - #{time_ago_in_words(event.event_at)} ago + - if event.event_at.kind_of?(Date) + #{distance_of_time_in_words(event.event_at, Time.zone.now.to_date)} ago + - else + #{time_ago_in_words(event.event_at)} ago - else unknown date diff --git a/app/views/photos/_card.html.haml b/app/views/photos/_card.html.haml index 7647837ad..5e395941e 100644 --- a/app/views/photos/_card.html.haml +++ b/app/views/photos/_card.html.haml @@ -5,7 +5,8 @@ %h5.ellipsis = photo_icon = link_to photo.title, photo_path(id: photo.id) - %i by #{link_to photo.owner_login_name, member_path(slug: photo.owner_slug)} + - if photo.owner_slug + %i by #{link_to photo.owner_login_name, member_path(slug: photo.owner_slug)} - if photo.date_taken.present? %small.text-muted %time{datetime: photo.date_taken}= I18n.l(photo.date_taken.to_date) diff --git a/app/views/photos/edit.html.haml b/app/views/photos/edit.html.haml index 33ecfac34..a421fc61d 100644 --- a/app/views/photos/edit.html.haml +++ b/app/views/photos/edit.html.haml @@ -5,6 +5,31 @@ = form_for(@photo) do |f| .form-group = f.label :title - = f.text_field :title, placeholder: "title" + = f.text_field :title, placeholder: "title", required: true + + .form-group + = f.label :thumbnail_url + = f.url_field :thumbnail_url + + .form-group + = f.label :fullsize_url + = f.url_field :fullsize_url + + .form-group + = f.label :link_url + = f.url_field :link_url + + .form-group + = f.label :license_name + = f.text_field :license_name + + .form-group + = f.label :license_url + = f.text_field :license_url + + .form-group + = f.label :date_taken + = f.datetime_field :date_taken + .form-group .form-actions= f.submit 'Save', class: 'btn' diff --git a/app/views/photos/index.html.haml b/app/views/photos/index.html.haml index baef72745..2d385b0be 100644 --- a/app/views/photos/index.html.haml +++ b/app/views/photos/index.html.haml @@ -9,7 +9,6 @@ - content_for :breadcrumbs do %li.breadcrumb-item= link_to 'Photos', photos_path -= page_entries_info @photos = will_paginate @photos .index-cards diff --git a/app/views/photos/new.html.haml b/app/views/photos/new.html.haml index 554e71aba..9a444619b 100644 --- a/app/views/photos/new.html.haml +++ b/app/views/photos/new.html.haml @@ -1,7 +1,7 @@ - content_for :title, "New Photo" %h1 New Photo -%h2 Choose photo for #{link_to @item, @item} +%h2 Choose photo for #{link_to @item, @item} from Flickr, or contribute to unique crops to iNaturalist or Pl@ntNet via the app. - if @please_reconnect_flickr %h2.alert Please reconnect your flickr account diff --git a/app/views/plant_parts/index.html.haml b/app/views/plant_parts/index.html.haml index 22c4784da..032a56f64 100644 --- a/app/views/plant_parts/index.html.haml +++ b/app/views/plant_parts/index.html.haml @@ -3,7 +3,7 @@ %h1 Plant Parts - if can? :create, PlantPart - = link_to 'New Plant part', new_plant_part_path, class: 'btn btn-info' + = link_to 'New plant part', new_plant_part_path, class: 'btn btn-info' .index-cards - @plant_parts.each do |plant_part| @@ -20,6 +20,8 @@ .card-footer %p - if can? :edit, plant_part - = link_to 'Edit', edit_plant_part_path(plant_part), class: 'btn btn-default btn-xs' + = link_to t('buttons.edit'), edit_plant_part_path(plant_part), class: 'btn btn-default btn-xs' - if can? :destroy, plant_part - = link_to 'Delete', plant_part, method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-default btn-xs' + = link_to t('buttons.delete'), plant_part, method: :delete, data: { confirm: t(:are_you_sure?) }, class: 'btn btn-default btn-xs' + += will_paginate(@plant_parts) diff --git a/app/views/plant_parts/show.html.haml b/app/views/plant_parts/show.html.haml index 012e7e706..94e4eac99 100644 --- a/app/views/plant_parts/show.html.haml +++ b/app/views/plant_parts/show.html.haml @@ -5,6 +5,7 @@ = tag("meta", property: "og:url", content: request.original_url) = tag("meta", property: "og:site_name", content: ENV['GROWSTUFF_SITE_NAME']) +%h1 #{@plant_part.name.titlecase} - if @plant_part.crops.empty? %p No crops are harvested for this plant part (yet). - else diff --git a/app/views/plantings/_modal.html.haml b/app/views/plantings/_modal.html.haml index 11d89ee32..c27a4fcef 100644 --- a/app/views/plantings/_modal.html.haml +++ b/app/views/plantings/_modal.html.haml @@ -14,7 +14,7 @@ = link_to plantings_path(planting: {crop_id: planting.crop_id, garden_id: garden.id}), method: :post do .md-v-line .d-flex.justify-content-between - = image_tag garden_image_path(garden), class: 'img', height: 50 + = image_tag garden_image_path(garden), class: 'img', height: 50, alt: garden.name %span %h4= garden.name %p= garden.description diff --git a/app/views/plantings/_photos.haml b/app/views/plantings/_photos.haml index 800b42828..c2eaafeae 100644 --- a/app/views/plantings/_photos.haml +++ b/app/views/plantings/_photos.haml @@ -10,5 +10,5 @@ - else %p No photos. - if can?(:edit, planting) && can?(:create, Photo) - %p Add a photo to visually track growth of this planting + %p Add a photo to visually track growth of this planting, to Flickr, iNaturalist or Pl@ntNet = add_photo_button(planting) diff --git a/app/views/plantings/index.html.haml b/app/views/plantings/index.html.haml index f20a0eba6..6a7cb4d09 100644 --- a/app/views/plantings/index.html.haml +++ b/app/views/plantings/index.html.haml @@ -30,6 +30,8 @@ = link_to (@owner ? member_plantings_path(@owner, format: format) : plantings_path(format: format)) do = icon 'fas', format.to_s = format.upcase + - if @owner + .badge.badge-info= link_to "iCal", member_plantings_path(@owner, format: 'ics', protocol: 'webcal', only_path: false) .badge.badge-success= link_to 'API Methods', '/api-docs' .col-md-10 diff --git a/app/views/plantings/index.ics.erb b/app/views/plantings/index.ics.erb new file mode 100644 index 000000000..c5f44b445 --- /dev/null +++ b/app/views/plantings/index.ics.erb @@ -0,0 +1,42 @@ +<% +# TODO Refactor to a Planting <-> Ical view class? +cal = Icalendar::Calendar.new +cal.description = "Plantings by #{@owner.login_name}" +@plantings.each do |planting| + + event = Icalendar::Event.new + + lines = [] + lines << "Quantity: #{planting['quantity'] ? planting['quantity'] : 'unknown' }" + lines << "Planted on: #{planting['planted_at'] ? planting['planted_at'] : 'unknown' }" + lines << "Sunniness: #{planting['sunniness'] ? planting['sunniness'] : 'unknown' }" + lines << "Planted from: #{planting['planted_from'] ? planting['planted_from'] : 'unknown' }" + lines << "First harvest from: #{planting['first_harvest_predicted_at'] ? planting['first_harvest_predicted_at'] : 'unknown' }" + lines << "Last harvest from: #{planting['last_harvest_predicted_at'] ? planting['last_harvest_predicted_at'] : 'unknown' }" + lines << "Finish predicted at: #{planting['finish_predicted_at'] ? planting['finish_predicted_at'] : 'unknown'}" + lines << "Finished at: #{planting['finished_at'] ? planting['finished_at'] : 'unknown' }" + + lines << planting.description + finish_date = Date.parse(planting['finished_at'] || planting['finish_predicted_at'] || planting['last_harvest_predicted_at']) rescue nil + + event.dtstart = Time.at(planting['created_at']) + event.dtend = finish_date || 1.day.from_now + event.summary = planting['crop_name'] + event.description = lines.join("\n") + event.ip_class = "PUBLIC" + event.url = planting_url(slug: planting['slug']) + + cal.add_event(event) + + if finish_date && finish_date > Date.today + todo = Icalendar::Todo.new + todo.dtstart = planting['first_harvest_predicted_at'] || finish_date || Date.today + todo.due = finish_date + todo.summary = "Harvest #{planting['crop_name']}" + + cal.add_todo(todo) + end +end +cal.publish +%> +<%= cal.to_ical %> diff --git a/app/views/plantings/show.html.haml b/app/views/plantings/show.html.haml index 44a099401..8a9d8739f 100644 --- a/app/views/plantings/show.html.haml +++ b/app/views/plantings/show.html.haml @@ -76,7 +76,7 @@ = link_to planting, class: 'list-group-item list-group-item-action flex-column align-items-start' do .d-flex.w-100.justify-content-between %p.mb-2 - = image_tag planting_image_path(planting), width: 75, class: 'rounded shadow' + = image_tag planting_image_path(planting), width: 75, class: 'rounded shadow', alt: "Image of #{planting.crop.name} by #{planting.owner}" .text-right %h5= planting.crop.name - if planting.planted_from.present? diff --git a/app/views/posts/_preview.haml b/app/views/posts/_preview.haml index 711618bd6..98f487156 100644 --- a/app/views/posts/_preview.haml +++ b/app/views/posts/_preview.haml @@ -1,7 +1,7 @@ .view = link_to post do - = image_tag post_image_path(post), class: 'img img-cover' + = image_tag post_image_path(post), class: 'img img-cover', alt: "A photo related to this post" %h4.font-weight-bold.mb-3 %strong = link_to post do @@ -16,5 +16,5 @@ = link_to crop do = crop_icon(crop) = crop.name.pluralize -/ = image_tag avatar_uri(post.author, 50), class: 'avatar' +/ = image_tag avatar_uri(post.author, 50), class: 'avatar', alt: post.author = link_to 'Read more', post, class: 'btn btn-rounded btn-md' diff --git a/app/views/posts/index.html.haml b/app/views/posts/index.html.haml index 6081aa608..b1376418c 100644 --- a/app/views/posts/index.html.haml +++ b/app/views/posts/index.html.haml @@ -9,10 +9,10 @@ %h1= @author ? t('.title.author_posts', author: @author) : t('.title.default') .row - .col-2 - .col-8= will_paginate @posts + .col-md-2 + .col-md-8= will_paginate @posts .row - .col-2 + .col-md-2 = render 'layouts/nav', model: Post %hr/ %p @@ -28,7 +28,7 @@ or = succeed "." do = link_to "comments RSS feed", comments_path(format: 'rss') - .col-10 + .col-md-10 .row.posts - @posts.each do |post| .col-lg-3.col-md-6.mb-3.post diff --git a/app/views/posts/show.rss.haml b/app/views/posts/show.rss.haml index f18e7df6b..664703acc 100644 --- a/app/views/posts/show.rss.haml +++ b/app/views/posts/show.rss.haml @@ -8,7 +8,7 @@ %title Comment by #{comment.author.login_name} on #{comment.created_at} %description - :escaped + :escaped_markdown

    Comment on #{ link_to @post.subject, post_url(@post) } diff --git a/app/views/scientific_names/_form.html.haml b/app/views/scientific_names/_form.html.haml index 95f768aa7..f10849497 100644 --- a/app/views/scientific_names/_form.html.haml +++ b/app/views/scientific_names/_form.html.haml @@ -1,4 +1,4 @@ -= form_for @scientific_name, html: { class: 'form-horizontal', role: "form" } do |f| += form_for @scientific_name, html: { class: 'form-horizontal' } do |f| - if @scientific_name.errors.any? #error_explanation %h2 diff --git a/app/views/scientific_names/index.html.haml b/app/views/scientific_names/index.html.haml index 6e6379c0c..b47c24540 100644 --- a/app/views/scientific_names/index.html.haml +++ b/app/views/scientific_names/index.html.haml @@ -17,8 +17,10 @@ %td= link_to 'Show', scientific_name %td - if can? :edit, scientific_name - = link_to 'Edit', edit_scientific_name_path(scientific_name), class: 'btn btn-default btn-xs' + = link_to t('buttons.edit'), edit_scientific_name_path(scientific_name), class: 'btn btn-default btn-xs' %td - if can? :destroy, scientific_name - = link_to 'Delete', scientific_name, method: :delete, data: { confirm: 'Are you sure?' }, + = link_to t('buttons.delete'), scientific_name, method: :delete, data: { confirm: t(:are_you_sure?) }, class: 'btn btn-default btn-xs' + += will_paginate @scientific_names diff --git a/app/views/timeline/_photos.html.haml b/app/views/timeline/_photos.html.haml index 732842c39..a9aae9389 100644 --- a/app/views/timeline/_photos.html.haml +++ b/app/views/timeline/_photos.html.haml @@ -1,5 +1,5 @@ .media - = link_to(image_tag(photo.thumbnail_url, width: 150, class: 'rounded'), photo) + = link_to(image_tag(photo.thumbnail_url, width: 150, class: 'rounded', alt: photo.title), photo) .media-body %p %ul.associations diff --git a/app/views/timeline/index.html.haml b/app/views/timeline/index.html.haml index f94cafd1b..3285db15b 100644 --- a/app/views/timeline/index.html.haml +++ b/app/views/timeline/index.html.haml @@ -16,7 +16,10 @@ = render 'timeline/photos', photo: resolve_model(event) if event.event_type == 'photo' %small - if event.event_at.present? - #{time_ago_in_words(event.event_at)} ago + - if event.event_at.kind_of?(Date) + #{distance_of_time_in_words(event.event_at, Time.zone.now.to_date)} ago + - else + #{time_ago_in_words(event.event_at)} ago - else unknown date diff --git a/config/application.rb b/config/application.rb index ef75d9372..7b18d9243 100644 --- a/config/application.rb +++ b/config/application.rb @@ -3,6 +3,8 @@ require_relative 'boot' require 'rails/all' +ENV['RAILS_DISABLE_DEPRECATED_TO_S_CONVERSION'] = "true" + require 'openssl' # Require the gems listed in Gemfile, including any gems @@ -16,8 +18,6 @@ module Growstuff I18n.config.enforce_available_locales = true - config.active_record.legacy_connection_handling = false - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. config.time_zone = 'UTC' diff --git a/config/environments/production.rb b/config/environments/production.rb index 0b161c458..8ba5acec9 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -26,8 +26,8 @@ Rails.application.configure do config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. - config.assets.js_compressor = Uglifier.new(harmony: true) - # config.assets.css_compressor = :sass + config.assets.js_compressor = :terser +# config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false diff --git a/config/environments/test.rb b/config/environments/test.rb index 14276a429..c2c4a4ecb 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -25,7 +25,7 @@ Rails.application.configure do config.action_controller.perform_caching = false # Raise exceptions instead of rendering exception templates. - config.action_dispatch.show_exceptions = false + config.action_dispatch.show_exceptions = :none # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 6e1d16f02..690e74d6d 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -3,3 +3,4 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf +Mime::Type.register "text/calendar", :ics diff --git a/config/initializers/rswag_api.rb b/config/initializers/rswag_api.rb index 307807a93..516a38820 100644 --- a/config/initializers/rswag_api.rb +++ b/config/initializers/rswag_api.rb @@ -5,7 +5,7 @@ Rswag::Api.configure do |c| # This is used by the Swagger middleware to serve requests for API descriptions # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure # that it's configured to generate files in the same folder - c.swagger_root = Rails.root.to_s + '/swagger' + c.openapi_root = Rails.root.to_s + '/swagger' # Inject a lamda function to alter the returned Swagger prior to serialization # The function will have access to the rack env for the current request diff --git a/config/initializers/rswag_ui.rb b/config/initializers/rswag_ui.rb index 8b632a43f..46d2ccc2e 100644 --- a/config/initializers/rswag_ui.rb +++ b/config/initializers/rswag_ui.rb @@ -7,5 +7,5 @@ Rswag::Ui.configure do |c| # NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as JSON or YAML endpoints, # then the list below should correspond to the relative paths for those endpoints - c.swagger_endpoint '/api-docs/v1/swagger.json', 'API V1 Docs' + c.openapi_endpoint '/api-docs/v1/swagger.json', 'API V1 Docs' end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 78030e471..3fa1481e0 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -25,7 +25,7 @@ en: invalid: 'Invalid email or password.' invalid_token: 'Invalid authentication token.' timeout: 'Your session expired, please sign in again to continue.' - inactive: 'Your account was not activated yet.' + inactive: 'Your account is not activated.' sessions: signed_in: 'Signed in successfully.' signed_out: 'Signed out successfully.' diff --git a/config/routes.rb b/config/routes.rb index e21ada9be..2ef2f8d0d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -53,7 +53,11 @@ Rails.application.routes.draw do get 'author/:author' => 'posts#index', as: 'by_author', on: :collection end - resources :scientific_names + resources :scientific_names do + collection do + get :gbif_suggest + end + end resources :alternate_names resources :plant_parts resources :photos @@ -73,6 +77,7 @@ Rails.application.routes.draw do get 'planted_from' => 'charts/crops#planted_from', constraints: { format: 'json' } get 'harvested_for' => 'charts/crops#harvested_for', constraints: { format: 'json' } post :openfarm + post :gbif collection do get 'requested' diff --git a/db/migrate/20171028230429_create_median_function.rb b/db/migrate/20171028230429_create_median_function.rb deleted file mode 100644 index ec86bced1..000000000 --- a/db/migrate/20171028230429_create_median_function.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -class CreateMedianFunction < ActiveRecord::Migration[4.2] - def up - # commented out, because we upgraded the gem later and this function was removed - # ActiveMedian.create_function - end - - def down - # ActiveMedian.drop_function - end -end diff --git a/db/migrate/20200815012538_remove_median_function.rb b/db/migrate/20200815012538_remove_median_function.rb deleted file mode 100644 index fd7118978..000000000 --- a/db/migrate/20200815012538_remove_median_function.rb +++ /dev/null @@ -1,6 +0,0 @@ -class RemoveMedianFunction < ActiveRecord::Migration[6.0] - def change - # No longer needed, after upgrading to activemedian 0.2.0 - ActiveMedian.drop_function - end -end diff --git a/db/migrate/20240114045751_add_gbif.rb b/db/migrate/20240114045751_add_gbif.rb new file mode 100644 index 000000000..3aabd9041 --- /dev/null +++ b/db/migrate/20240114045751_add_gbif.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddGbif < ActiveRecord::Migration[7.0] + def change + add_column :scientific_names, :gbif_key, :int + add_column :scientific_names, :gbif_rank, :string + add_column :scientific_names, :gbif_status, :string + add_column :scientific_names, :wikidata_id, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 3cd83979d..5251aff60 100644 --- 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.0].define(version: 2023_03_13_015323) do +ActiveRecord::Schema[7.0].define(version: 2024_01_14_045751) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -543,6 +543,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_03_13_015323) do t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil t.integer "creator_id" + t.integer "gbif_key" + t.string "gbif_rank" + t.string "gbif_status" + t.string "wikidata_id" end create_table "seeds", id: :serial, force: :cascade do |t| diff --git a/env-example b/env-example index 951611496..26db16165 100644 --- a/env-example +++ b/env-example @@ -10,7 +10,7 @@ # include: # mapbox_map_id -# To use it, copy application.yml.example to application.yml (which is +# To use it, copy env-example.yml or application.yml.example to application.yml (which is # .gitignored) and fill in the appropriate values. # Settings in this file will be available to you as ENV['WHATEVER'] @@ -59,3 +59,14 @@ GROWSTUFF_ELASTICSEARCH="true" GROWSTUFF_EMAIL='noreply@dev.growstuff.org' ELASTIC_SEARCH_VERSION="7.5.1-amd64" +# We also now use SMTP2GO in prod and Mailgun in staging +# and recaptcha to solve our email issues after SendGrid stopped working +MAILGUN_SMTP_LOGIN="" +MAILGUN_SMTP_PASSWORD="" +MAILGUN_SMTP_PORT="" +MAILGUN_SMTP_SERVER="" +# These recaptcha values are the official Google test ones from +# https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do +# In production, replace them with real ones +RECAPTCHA_SITE_KEY="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI" +RECAPTCHA_SECRET_KEY="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" diff --git a/lib/haml/filters/escaped_markdown.rb b/lib/haml/filters/escaped_markdown.rb index d68f0661d..bc219a72a 100644 --- a/lib/haml/filters/escaped_markdown.rb +++ b/lib/haml/filters/escaped_markdown.rb @@ -3,15 +3,14 @@ require 'bluecloth' require 'haml/filters/growstuff_markdown' -module Haml::Filters - module EscapedMarkdown - include Haml::Filters::Base - def render(text) - Haml::Helpers.html_escape Haml::Filters::GrowstuffMarkdown.render(text) +class Haml::Filters + class EscapedMarkdown < Haml::Filters::GrowstuffMarkdown + def compile(node) + [:escape, true, super(node)] end end # Register it as the handler for the :escaped_markdown HAML command. # The automatic system gives us :escapedmarkdown, which is ugly. - defined['escaped_markdown'] = EscapedMarkdown + Haml::Filters.registered[:escaped_markdown] ||= EscapedMarkdown end diff --git a/lib/haml/filters/growstuff_markdown.rb b/lib/haml/filters/growstuff_markdown.rb index 88037fac2..1304e8171 100644 --- a/lib/haml/filters/growstuff_markdown.rb +++ b/lib/haml/filters/growstuff_markdown.rb @@ -2,15 +2,15 @@ require 'bluecloth' -module Haml::Filters - module GrowstuffMarkdown - include Haml::Filters::Base +class Haml::Filters + class GrowstuffMarkdown < Haml::Filters::Markdown - def render(text) - @expanded = text + def compile(node) + @expanded = node.value[:text] expand_crops! expand_members! - BlueCloth.new(@expanded).to_html + node.value[:text] = @expanded + compile_with_tilt(node, 'markdown') end private @@ -72,5 +72,5 @@ module Haml::Filters # Register it as the handler for the :growstuff_markdown HAML command. # The automatic system gives us :growstuffmarkdown, which is ugly. - defined['growstuff_markdown'] = GrowstuffMarkdown + Haml::Filters.registered[:growstuff_markdown] = GrowstuffMarkdown end diff --git a/lib/tasks/gbif.rake b/lib/tasks/gbif.rake new file mode 100644 index 000000000..4fbdb85f2 --- /dev/null +++ b/lib/tasks/gbif.rake @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +namespace :gbif do + desc "Retrieve crop info from GBIF" + + task import: :environment do + Rails.logger = Logger.new(STDOUT) + GbifService.new.import! + end +end diff --git a/spec/cassettes/GbifService/_fetch/fetches_a_given_key.yml b/spec/cassettes/GbifService/_fetch/fetches_a_given_key.yml new file mode 100644 index 000000000..7e5560056 --- /dev/null +++ b/spec/cassettes/GbifService/_fetch/fetches_a_given_key.yml @@ -0,0 +1,57 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.gbif.org/v1/species/2930137 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday/v1.10.3 Gbif/v0.2.0 + X-USER-AGENT: + - Faraday/v1.10.3 Gbif/v0.2.0 + response: + status: + code: 200 + message: OK + headers: + vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + x-content-type-options: + - nosniff + x-xss-protection: + - 1; mode=block + pragma: + - no-cache + expires: + - '0' + x-frame-options: + - DENY + content-type: + - application/json + date: + - Sun, 14 Jan 2024 10:03:58 GMT + cache-control: + - public, max-age=3601 + x-varnish: + - 952863014 979042621 + age: + - '126' + via: + - 1.1 varnish (Varnish/6.0) + accept-ranges: + - bytes + content-length: + - '938' + connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"key":2930137,"nubKey":2930137,"nameKey":10463714,"taxonID":"gbif:2930137","kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"datasetKey":"d7dddbf4-2cf0-4f39-9b2a-bb099caae36c","constituentKey":"7ddf754f-d193-4cc9-b351-99906754a03b","parentKey":2928997,"parent":"Solanum","scientificName":"Solanum + lycopersicum L.","canonicalName":"Solanum lycopersicum","vernacularName":"Garden + tomato","authorship":"L.","nameType":"SCIENTIFIC","rank":"SPECIES","origin":"SOURCE","taxonomicStatus":"ACCEPTED","nomenclaturalStatus":[],"remarks":"","publishedIn":"L. + (1753). In: Sp. Pl. 185.","numDescendants":23,"lastCrawled":"2023-08-22T23:20:59.545+00:00","lastInterpreted":"2023-08-22T23:12:05.487+00:00","issues":[],"class":"Magnoliopsida"}' + recorded_at: Sun, 14 Jan 2024 10:06:05 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/cassettes/GbifService/_suggest/matches.yml b/spec/cassettes/GbifService/_suggest/matches.yml new file mode 100644 index 000000000..09ff36fb5 --- /dev/null +++ b/spec/cassettes/GbifService/_suggest/matches.yml @@ -0,0 +1,103 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.gbif.org/v1/species/suggest?limit=100&q=Solanum+lycopersicum + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday/v1.10.3 Gbif/v0.2.0 + X-USER-AGENT: + - Faraday/v1.10.3 Gbif/v0.2.0 + response: + status: + code: 200 + message: OK + headers: + vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + x-content-type-options: + - nosniff + x-xss-protection: + - 1; mode=block + pragma: + - no-cache + expires: + - '0' + x-frame-options: + - DENY + content-type: + - application/json + date: + - Sun, 14 Jan 2024 10:06:04 GMT + cache-control: + - public, max-age=3601 + x-varnish: + - '993984559' + age: + - '0' + via: + - 1.1 varnish (Varnish/6.0) + accept-ranges: + - bytes + content-length: + - '10730' + connection: + - keep-alive + body: + encoding: UTF-8 + string: '[{"key":2930137,"nameKey":10463714,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum","parentKey":2928997,"nubKey":2930137,"scientificName":"Solanum + lycopersicum L.","canonicalName":"Solanum lycopersicum","rank":"SPECIES","status":"ACCEPTED","synonym":false,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum"},"class":"Magnoliopsida"},{"key":7815295,"nameKey":31973001,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":7815295,"parent":"Solanum","parentKey":2928997,"nubKey":7815295,"scientificName":"Solanum + lycopersicum Blanco, 1837","canonicalName":"Solanum lycopersicum","rank":"SPECIES","status":"DOUBTFUL","synonym":false,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum"},"class":"Magnoliopsida"},{"key":8586238,"nameKey":6531517,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Lycopersicum + solanum-lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":8586238,"parent":"Solanum","parentKey":2928997,"nubKey":8586238,"scientificName":"Lycopersicum + solanum-lycopersicum Hill","canonicalName":"Lycopersicum solanum-lycopersicum","rank":"SPECIES","status":"DOUBTFUL","synonym":false,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum"},"class":"Magnoliopsida"},{"key":8640337,"nameKey":6531515,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Lycopersicum + solanum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":8640337,"parent":"Solanum","parentKey":2928997,"nubKey":8640337,"scientificName":"Lycopersicum + solanum Medik.","canonicalName":"Lycopersicum solanum","rank":"SPECIES","status":"DOUBTFUL","synonym":false,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum"},"class":"Magnoliopsida"},{"key":7608359,"nameKey":6531353,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum","parentKey":2928997,"nubKey":7608359,"scientificName":"Lycopersicon + solanum-lycopersicum Hill","canonicalName":"Lycopersicon solanum-lycopersicum","rank":"SPECIES","status":"SYNONYM","synonym":true,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum","2930137":"Solanum + lycopersicum"},"class":"Magnoliopsida"},{"key":11519041,"nameKey":97469846,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum + lycopersicum","parentKey":2930137,"nubKey":11519041,"scientificName":"Solanum + lycopersicum subsp. lycopersicum","canonicalName":"Solanum lycopersicum lycopersicum","rank":"SUBSPECIES","status":"ACCEPTED","synonym":false,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum","2930137":"Solanum + lycopersicum"},"class":"Magnoliopsida"},{"key":11760453,"nameKey":97469847,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum + lycopersicum","parentKey":2930137,"nubKey":11760453,"scientificName":"Solanum + lycopersicum subsp. cerasiforme (Alef.) Voss","canonicalName":"Solanum lycopersicum + cerasiforme","rank":"SUBSPECIES","status":"ACCEPTED","synonym":false,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum","2930137":"Solanum + lycopersicum"},"class":"Magnoliopsida"},{"key":7904703,"nameKey":10463761,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum + lycopersicum","parentKey":2930137,"nubKey":7904703,"scientificName":"Solanum + lycopersicum var. cerasiforme (Alef.) Voss","canonicalName":"Solanum lycopersicum + cerasiforme","rank":"VARIETY","status":"ACCEPTED","synonym":false,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum","2930137":"Solanum + lycopersicum"},"class":"Magnoliopsida"},{"key":11014233,"nameKey":36721070,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum","parentKey":2928997,"nubKey":11014233,"scientificName":"Solanum + lycopersicum var. piriforme (Alef.) Voss","canonicalName":"Solanum lycopersicum + piriforme","rank":"VARIETY","status":"SYNONYM","synonym":true,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum","2930137":"Solanum + lycopersicum"},"class":"Magnoliopsida"},{"key":10798260,"nameKey":36720428,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum","parentKey":2928997,"nubKey":10798260,"scientificName":"Solanum + lycopersicum var. ribisiodes Voss","canonicalName":"Solanum lycopersicum ribisiodes","rank":"VARIETY","status":"SYNONYM","synonym":true,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum","2930137":"Solanum + lycopersicum"},"class":"Magnoliopsida"},{"key":2930169,"nameKey":10463787,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum","parentKey":2928997,"nubKey":2930169,"scientificName":"Solanum + lycopersicum var. esculentum (Mill.) Voss","canonicalName":"Solanum lycopersicum + esculentum","rank":"VARIETY","status":"SYNONYM","synonym":true,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum","2930137":"Solanum + lycopersicum"},"class":"Magnoliopsida"},{"key":4274699,"nameKey":10463795,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum","parentKey":2928997,"nubKey":4274699,"scientificName":"Solanum + lycopersicum var. lycopersicum","canonicalName":"Solanum lycopersicum lycopersicum","rank":"VARIETY","status":"SYNONYM","synonym":true,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum","2930137":"Solanum + lycopersicum"},"class":"Magnoliopsida"},{"key":8118741,"nameKey":10463759,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum","parentKey":2928997,"nubKey":8118741,"scientificName":"Solanum + lycopersicum var. cerasiforme (Alef.) Fosberg","canonicalName":"Solanum lycopersicum + cerasiforme","rank":"VARIETY","status":"SYNONYM","synonym":true,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum","2930137":"Solanum + lycopersicum"},"class":"Magnoliopsida"},{"key":2930179,"nameKey":10463798,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum","parentKey":2928997,"nubKey":2930179,"scientificName":"Solanum + lycopersicum var. oviforme Voss","canonicalName":"Solanum lycopersicum oviforme","rank":"VARIETY","status":"SYNONYM","synonym":true,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum","2930137":"Solanum + lycopersicum"},"class":"Magnoliopsida"},{"key":2930165,"nameKey":10463770,"kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"parent":"Solanum","parentKey":2928997,"nubKey":2930165,"scientificName":"Solanum + lycopersicum var. cerasiforme (Dunal) D.M.Spooner, G.J.Anderson & R.K.Jansen","canonicalName":"Solanum + lycopersicum cerasiforme","rank":"VARIETY","status":"SYNONYM","synonym":true,"higherClassificationMap":{"6":"Plantae","7707728":"Tracheophyta","220":"Magnoliopsida","1176":"Solanales","7717":"Solanaceae","2928997":"Solanum","2930137":"Solanum + lycopersicum"},"class":"Magnoliopsida"}]' + recorded_at: Sun, 14 Jan 2024 10:06:04 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/cassettes/GbifService/_update_crop/gets_photos.yml b/spec/cassettes/GbifService/_update_crop/gets_photos.yml new file mode 100644 index 000000000..2e116a00b --- /dev/null +++ b/spec/cassettes/GbifService/_update_crop/gets_photos.yml @@ -0,0 +1,108 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.gbif.org/v1/species/2930137 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday/v1.10.3 Gbif/v0.2.0 + X-USER-AGENT: + - Faraday/v1.10.3 Gbif/v0.2.0 + response: + status: + code: 200 + message: OK + headers: + vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + x-content-type-options: + - nosniff + x-xss-protection: + - 1; mode=block + pragma: + - no-cache + expires: + - '0' + x-frame-options: + - DENY + content-type: + - application/json + date: + - Sun, 14 Jan 2024 10:03:58 GMT + cache-control: + - public, max-age=3601 + x-varnish: + - 926782876 979042621 + age: + - '830' + via: + - 1.1 varnish (Varnish/6.0) + accept-ranges: + - bytes + content-length: + - '938' + connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"key":2930137,"nubKey":2930137,"nameKey":10463714,"taxonID":"gbif:2930137","kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"datasetKey":"d7dddbf4-2cf0-4f39-9b2a-bb099caae36c","constituentKey":"7ddf754f-d193-4cc9-b351-99906754a03b","parentKey":2928997,"parent":"Solanum","scientificName":"Solanum + lycopersicum L.","canonicalName":"Solanum lycopersicum","vernacularName":"Garden + tomato","authorship":"L.","nameType":"SCIENTIFIC","rank":"SPECIES","origin":"SOURCE","taxonomicStatus":"ACCEPTED","nomenclaturalStatus":[],"remarks":"","publishedIn":"L. + (1753). In: Sp. Pl. 185.","numDescendants":23,"lastCrawled":"2023-08-22T23:20:59.545+00:00","lastInterpreted":"2023-08-22T23:12:05.487+00:00","issues":[],"class":"Magnoliopsida"}' + recorded_at: Sun, 14 Jan 2024 10:17:49 GMT +- request: + method: get + uri: https://api.gbif.org/v1/occurrence/search?hasCoordinate=true&limit=3&mediatype=StillImage&offset=0&taxonKey=2930137 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday/v1.10.3 Gbif/v0.2.0 + X-USER-AGENT: + - Faraday/v1.10.3 Gbif/v0.2.0 + response: + status: + code: 200 + message: OK + headers: + vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + x-content-type-options: + - nosniff + x-xss-protection: + - 1; mode=block + pragma: + - no-cache + expires: + - '0' + x-frame-options: + - DENY + content-type: + - application/json + date: + - Sun, 14 Jan 2024 10:15:24 GMT + cache-control: + - public, max-age=600 + x-varnish: + - 972948084 990052778 + age: + - '145' + via: + - 1.1 varnish (Varnish/6.0) + accept-ranges: + - bytes + content-length: + - '12391' + connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: !binary |- + eyJvZmZzZXQiOjAsImxpbWl0IjozLCJlbmRPZlJlY29yZHMiOmZhbHNlLCJjb3VudCI6NzY0NCwicmVzdWx0cyI6W3sia2V5Ijo0NTA3Njg4MTMwLCJkYXRhc2V0S2V5IjoiNTBjOTUwOWQtMjJjNy00YTIyLWE0N2QtOGM0ODQyNWVmNGE3IiwicHVibGlzaGluZ09yZ0tleSI6IjI4ZWIxYTNmLTFjMTUtNGE5NS05MzFhLTRhZjkwZWNiNTc0ZCIsImluc3RhbGxhdGlvbktleSI6Ijk5NzQ0OGE4LWY3NjItMTFlMS1hNDM5LTAwMTQ1ZWI0NWU5YSIsImhvc3RpbmdPcmdhbml6YXRpb25LZXkiOiIyOGViMWEzZi0xYzE1LTRhOTUtOTMxYS00YWY5MGVjYjU3NGQiLCJwdWJsaXNoaW5nQ291bnRyeSI6IlVTIiwicHJvdG9jb2wiOiJEV0NfQVJDSElWRSIsImxhc3RDcmF3bGVkIjoiMjAyNC0wMS0wOVQwMzozMzozOC43MzUrMDA6MDAiLCJsYXN0UGFyc2VkIjoiMjAyNC0wMS0wOVQxMzoyNjowMy44OTErMDA6MDAiLCJjcmF3bElkIjo0MjYsImV4dGVuc2lvbnMiOnsiaHR0cDovL3JzLmdiaWYub3JnL3Rlcm1zLzEuMC9NdWx0aW1lZGlhIjpbeyJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRlZCI6IjIwMjQtMDEtMDFUMTE6MDc6MDBaIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2xpY2Vuc2UiOiJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS1uYy80LjAvIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL3R5cGUiOiJTdGlsbEltYWdlIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2Zvcm1hdCI6ImltYWdlL2pwZWciLCJodHRwOi8vcnMudGR3Zy5vcmcvZHdjL3Rlcm1zL2NhdGFsb2dOdW1iZXIiOiIzNDM4NzQzNTAiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9waG90b3MvMzQzODc0MzUwIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL3B1Ymxpc2hlciI6ImlOYXR1cmFsaXN0IiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL3JpZ2h0c0hvbGRlciI6IkluZ2Vib3JnIHZhbiBMZWV1d2VuIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2NyZWF0b3IiOiJJbmdlYm9yZyB2YW4gTGVldXdlbiIsImh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9pZGVudGlmaWVyIjoiaHR0cHM6Ly9pbmF0dXJhbGlzdC1vcGVuLWRhdGEuczMuYW1hem9uYXdzLmNvbS9waG90b3MvMzQzODc0MzUwL29yaWdpbmFsLmpwZWcifV19LCJiYXNpc09mUmVjb3JkIjoiSFVNQU5fT0JTRVJWQVRJT04iLCJvY2N1cnJlbmNlU3RhdHVzIjoiUFJFU0VOVCIsInRheG9uS2V5IjoyOTMwMTM3LCJraW5nZG9tS2V5Ijo2LCJwaHlsdW1LZXkiOjc3MDc3MjgsImNsYXNzS2V5IjoyMjAsIm9yZGVyS2V5IjoxMTc2LCJmYW1pbHlLZXkiOjc3MTcsImdlbnVzS2V5IjoyOTI4OTk3LCJzcGVjaWVzS2V5IjoyOTMwMTM3LCJhY2NlcHRlZFRheG9uS2V5IjoyOTMwMTM3LCJzY2llbnRpZmljTmFtZSI6IlNvbGFudW0gbHljb3BlcnNpY3VtIEwuIiwiYWNjZXB0ZWRTY2llbnRpZmljTmFtZSI6IlNvbGFudW0gbHljb3BlcnNpY3VtIEwuIiwia2luZ2RvbSI6IlBsYW50YWUiLCJwaHlsdW0iOiJUcmFjaGVvcGh5dGEiLCJvcmRlciI6IlNvbGFuYWxlcyIsImZhbWlseSI6IlNvbGFuYWNlYWUiLCJnZW51cyI6IlNvbGFudW0iLCJzcGVjaWVzIjoiU29sYW51bSBseWNvcGVyc2ljdW0iLCJnZW5lcmljTmFtZSI6IlNvbGFudW0iLCJzcGVjaWZpY0VwaXRoZXQiOiJseWNvcGVyc2ljdW0iLCJ0YXhvblJhbmsiOiJTUEVDSUVTIiwidGF4b25vbWljU3RhdHVzIjoiQUNDRVBURUQiLCJpdWNuUmVkTGlzdENhdGVnb3J5IjoiTkUiLCJkYXRlSWRlbnRpZmllZCI6IjIwMjQtMDEtMDFUMTM6MDA6MjciLCJkZWNpbWFsTGF0aXR1ZGUiOjI4LjM5MDk2NCwiZGVjaW1hbExvbmdpdHVkZSI6LTE2LjYyNjY5NywiY29vcmRpbmF0ZVVuY2VydGFpbnR5SW5NZXRlcnMiOjQuMCwiY29udGluZW50IjoiQUZSSUNBIiwic3RhdGVQcm92aW5jZSI6IklzbGFzIENhbmFyaWFzIiwiZ2FkbSI6eyJsZXZlbDAiOnsiZ2lkIjoiRVNQIiwibmFtZSI6IlNwYWluIn0sImxldmVsMSI6eyJnaWQiOiJFU1AuMTRfMSIsIm5hbWUiOiJJc2xhcyBDYW5hcmlhcyJ9LCJsZXZlbDIiOnsiZ2lkIjoiRVNQLjE0LjJfMSIsIm5hbWUiOiJTYW50YSBDcnV6IGRlIFRlbmVyaWZlIn0sImxldmVsMyI6eyJnaWQiOiJFU1AuMTQuMi4xXzEiLCJuYW1lIjoibi5hLiAoMTQ0KSJ9fSwieWVhciI6MjAyNCwibW9udGgiOjEsImRheSI6MSwiZXZlbnREYXRlIjoiMjAyNC0wMS0wMVQxMTowNzowMCIsImlzc3VlcyI6WyJDT09SRElOQVRFX1JPVU5ERUQiLCJDT05USU5FTlRfREVSSVZFRF9GUk9NX0NPT1JESU5BVEVTIiwiVEFYT05fTUFUQ0hfVEFYT05fSURfSUdOT1JFRCJdLCJtb2RpZmllZCI6IjIwMjQtMDEtMDFUMTY6MzU6MDQuMDAwKzAwOjAwIiwibGFzdEludGVycHJldGVkIjoiMjAyNC0wMS0wOVQxMzoyNjowMy44OTErMDA6MDAiLCJyZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL29ic2VydmF0aW9ucy8xOTU0NjIyNjEiLCJsaWNlbnNlIjoiaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMvNC4wL2xlZ2FsY29kZSIsImlkZW50aWZpZXJzIjpbeyJpZGVudGlmaWVyIjoiMTk1NDYyMjYxIn1dLCJtZWRpYSI6W3sidHlwZSI6IlN0aWxsSW1hZ2UiLCJmb3JtYXQiOiJpbWFnZS9qcGVnIiwicmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9waG90b3MvMzQzODc0MzUwIiwiY3JlYXRlZCI6IjIwMjQtMDEtMDFUMTE6MDc6MDAuMDAwKzAwOjAwIiwiY3JlYXRvciI6IkluZ2Vib3JnIHZhbiBMZWV1d2VuIiwicHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJsaWNlbnNlIjoiaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMvNC4wLyIsInJpZ2h0c0hvbGRlciI6IkluZ2Vib3JnIHZhbiBMZWV1d2VuIiwiaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzM0Mzg3NDM1MC9vcmlnaW5hbC5qcGVnIn1dLCJmYWN0cyI6W10sInJlbGF0aW9ucyI6W10sImlzSW5DbHVzdGVyIjpmYWxzZSwiZGF0YXNldE5hbWUiOiJpTmF0dXJhbGlzdCByZXNlYXJjaC1ncmFkZSBvYnNlcnZhdGlvbnMiLCJyZWNvcmRlZEJ5IjoiSW5nZWJvcmcgdmFuIExlZXV3ZW4iLCJpZGVudGlmaWVkQnkiOiJJbmdlYm9yZyB2YW4gTGVldXdlbiIsImdlb2RldGljRGF0dW0iOiJXR1M4NCIsImNsYXNzIjoiTWFnbm9saW9wc2lkYSIsImNvdW50cnlDb2RlIjoiRVMiLCJyZWNvcmRlZEJ5SURzIjpbXSwiaWRlbnRpZmllZEJ5SURzIjpbXSwiY291bnRyeSI6IlNwYWluIiwicmlnaHRzSG9sZGVyIjoiSW5nZWJvcmcgdmFuIExlZXV3ZW4iLCJpZGVudGlmaWVyIjoiMTk1NDYyMjYxIiwiaHR0cDovL3Vua25vd24ub3JnL25pY2siOiJ3aWxkY2hyb21hIiwidmVyYmF0aW1FdmVudERhdGUiOiIyMDI0LTAxLTAxIDExOjA3OjAwIiwiZ2JpZklEIjoiNDUwNzY4ODEzMCIsInZlcmJhdGltTG9jYWxpdHkiOiJCYXJyYW5jbyBkZSBSdWl6LCAzODQyMCwgU2FudGEgQ3J1eiBkZSBUZW5lcmlmZSwgU3BhaW4iLCJjb2xsZWN0aW9uQ29kZSI6Ik9ic2VydmF0aW9ucyIsIm9jY3VycmVuY2VJRCI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9vYnNlcnZhdGlvbnMvMTk1NDYyMjYxIiwidGF4b25JRCI6IjUxNzM3IiwiY2F0YWxvZ051bWJlciI6IjE5NTQ2MjI2MSIsImluc3RpdHV0aW9uQ29kZSI6ImlOYXR1cmFsaXN0IiwiZXZlbnRUaW1lIjoiMTE6MDc6MDArMDA6MDAiLCJodHRwOi8vdW5rbm93bi5vcmcvY2FwdGl2ZSI6IndpbGQiLCJpZGVudGlmaWNhdGlvbklEIjoiNDM5NTkxNDI0In0seyJrZXkiOjQ1MDc5NTMzODcsImRhdGFzZXRLZXkiOiI1MGM5NTA5ZC0yMmM3LTRhMjItYTQ3ZC04YzQ4NDI1ZWY0YTciLCJwdWJsaXNoaW5nT3JnS2V5IjoiMjhlYjFhM2YtMWMxNS00YTk1LTkzMWEtNGFmOTBlY2I1NzRkIiwiaW5zdGFsbGF0aW9uS2V5IjoiOTk3NDQ4YTgtZjc2Mi0xMWUxLWE0MzktMDAxNDVlYjQ1ZTlhIiwiaG9zdGluZ09yZ2FuaXphdGlvbktleSI6IjI4ZWIxYTNmLTFjMTUtNGE5NS05MzFhLTRhZjkwZWNiNTc0ZCIsInB1Ymxpc2hpbmdDb3VudHJ5IjoiVVMiLCJwcm90b2NvbCI6IkRXQ19BUkNISVZFIiwibGFzdENyYXdsZWQiOiIyMDI0LTAxLTA5VDAzOjMzOjM4LjczNSswMDowMCIsImxhc3RQYXJzZWQiOiIyMDI0LTAxLTA5VDEzOjQ1OjI0Ljk2MSswMDowMCIsImNyYXdsSWQiOjQyNiwiZXh0ZW5zaW9ucyI6eyJodHRwOi8vcnMuZ2JpZi5vcmcvdGVybXMvMS4wL011bHRpbWVkaWEiOlt7Imh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9jcmVhdGVkIjoiMjAyNC0wMS0wMlQwNzozMTowNFoiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvbGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLzQuMC8iLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvdHlwZSI6IlN0aWxsSW1hZ2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvZm9ybWF0IjoiaW1hZ2UvanBlZyIsImh0dHA6Ly9ycy50ZHdnLm9yZy9kd2MvdGVybXMvY2F0YWxvZ051bWJlciI6IjM0NDA1ODExMyIsImh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9yZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL3Bob3Rvcy8zNDQwNTgxMTMiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmlnaHRzSG9sZGVyIjoid2Fud2lzYSBcdUQ4M0RcdURDOUUiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRvciI6Indhbndpc2EgXHVEODNEXHVEQzlFIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2lkZW50aWZpZXIiOiJodHRwczovL2luYXR1cmFsaXN0LW9wZW4tZGF0YS5zMy5hbWF6b25hd3MuY29tL3Bob3Rvcy8zNDQwNTgxMTMvb3JpZ2luYWwuanBlZyJ9XX0sImJhc2lzT2ZSZWNvcmQiOiJIVU1BTl9PQlNFUlZBVElPTiIsIm9jY3VycmVuY2VTdGF0dXMiOiJQUkVTRU5UIiwidGF4b25LZXkiOjI5MzAxMzcsImtpbmdkb21LZXkiOjYsInBoeWx1bUtleSI6NzcwNzcyOCwiY2xhc3NLZXkiOjIyMCwib3JkZXJLZXkiOjExNzYsImZhbWlseUtleSI6NzcxNywiZ2VudXNLZXkiOjI5Mjg5OTcsInNwZWNpZXNLZXkiOjI5MzAxMzcsImFjY2VwdGVkVGF4b25LZXkiOjI5MzAxMzcsInNjaWVudGlmaWNOYW1lIjoiU29sYW51bSBseWNvcGVyc2ljdW0gTC4iLCJhY2NlcHRlZFNjaWVudGlmaWNOYW1lIjoiU29sYW51bSBseWNvcGVyc2ljdW0gTC4iLCJraW5nZG9tIjoiUGxhbnRhZSIsInBoeWx1bSI6IlRyYWNoZW9waHl0YSIsIm9yZGVyIjoiU29sYW5hbGVzIiwiZmFtaWx5IjoiU29sYW5hY2VhZSIsImdlbnVzIjoiU29sYW51bSIsInNwZWNpZXMiOiJTb2xhbnVtIGx5Y29wZXJzaWN1bSIsImdlbmVyaWNOYW1lIjoiU29sYW51bSIsInNwZWNpZmljRXBpdGhldCI6Imx5Y29wZXJzaWN1bSIsInRheG9uUmFuayI6IlNQRUNJRVMiLCJ0YXhvbm9taWNTdGF0dXMiOiJBQ0NFUFRFRCIsIml1Y25SZWRMaXN0Q2F0ZWdvcnkiOiJORSIsImRhdGVJZGVudGlmaWVkIjoiMjAyNC0wMS0wMlQwNzozNjoxNyIsImRlY2ltYWxMYXRpdHVkZSI6MTcuMTYxMjg1LCJkZWNpbWFsTG9uZ2l0dWRlIjoxMDQuNzc0NTIxLCJjb250aW5lbnQiOiJBU0lBIiwic3RhdGVQcm92aW5jZSI6Ik5ha2hvbiBQaGFub20iLCJnYWRtIjp7ImxldmVsMCI6eyJnaWQiOiJUSEEiLCJuYW1lIjoiVGhhaWxhbmQifSwibGV2ZWwxIjp7ImdpZCI6IlRIQS4yOF8xIiwibmFtZSI6Ik5ha2hvbiBQaGFub20ifSwibGV2ZWwyIjp7ImdpZCI6IlRIQS4yOC4zXzEiLCJuYW1lIjoiTXVhbmcgTmFraG9uIFBoYW5vbSJ9LCJsZXZlbDMiOnsiZ2lkIjoiVEhBLjI4LjMuMl8xIiwibmFtZSI6IkJhbiBLbGFuZyJ9fSwieWVhciI6MjAyNCwibW9udGgiOjEsImRheSI6MiwiZXZlbnREYXRlIjoiMjAyNC0wMS0wMlQxNDozMTowNCIsImlzc3VlcyI6WyJDT09SRElOQVRFX1JPVU5ERUQiLCJDT05USU5FTlRfREVSSVZFRF9GUk9NX0NPT1JESU5BVEVTIiwiVEFYT05fTUFUQ0hfVEFYT05fSURfSUdOT1JFRCJdLCJtb2RpZmllZCI6IjIwMjQtMDEtMDJUMDc6NDA6MDUuMDAwKzAwOjAwIiwibGFzdEludGVycHJldGVkIjoiMjAyNC0wMS0wOVQxMzo0NToyNC45NjErMDA6MDAiLCJyZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL29ic2VydmF0aW9ucy8xOTU1NTE1NTkiLCJsaWNlbnNlIjoiaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMvNC4wL2xlZ2FsY29kZSIsImlkZW50aWZpZXJzIjpbeyJpZGVudGlmaWVyIjoiMTk1NTUxNTU5In1dLCJtZWRpYSI6W3sidHlwZSI6IlN0aWxsSW1hZ2UiLCJmb3JtYXQiOiJpbWFnZS9qcGVnIiwicmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9waG90b3MvMzQ0MDU4MTEzIiwiY3JlYXRlZCI6IjIwMjQtMDEtMDJUMDc6MzE6MDQuMDAwKzAwOjAwIiwiY3JlYXRvciI6Indhbndpc2EgXHVEODNEXHVEQzlFIiwicHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJsaWNlbnNlIjoiaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMvNC4wLyIsInJpZ2h0c0hvbGRlciI6Indhbndpc2EgXHVEODNEXHVEQzlFIiwiaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzM0NDA1ODExMy9vcmlnaW5hbC5qcGVnIn1dLCJmYWN0cyI6W10sInJlbGF0aW9ucyI6W10sImlzSW5DbHVzdGVyIjpmYWxzZSwiZGF0YXNldE5hbWUiOiJpTmF0dXJhbGlzdCByZXNlYXJjaC1ncmFkZSBvYnNlcnZhdGlvbnMiLCJyZWNvcmRlZEJ5Ijoid2Fud2lzYSBcdUQ4M0RcdURDOUUiLCJpZGVudGlmaWVkQnkiOiJ3YW53aXNhIFx1RDgzRFx1REM5RSIsImdlb2RldGljRGF0dW0iOiJXR1M4NCIsImNsYXNzIjoiTWFnbm9saW9wc2lkYSIsImNvdW50cnlDb2RlIjoiVEgiLCJyZWNvcmRlZEJ5SURzIjpbXSwiaWRlbnRpZmllZEJ5SURzIjpbXSwiY291bnRyeSI6IlRoYWlsYW5kIiwicmlnaHRzSG9sZGVyIjoid2Fud2lzYSBcdUQ4M0RcdURDOUUiLCJpZGVudGlmaWVyIjoiMTk1NTUxNTU5IiwiaHR0cDovL3Vua25vd24ub3JnL25pY2siOiJ3YW53aXNhX3BhcmVlc29pIiwidmVyYmF0aW1FdmVudERhdGUiOiIyMDI0LTAxLTAyIDE0OjMxOjA0IiwiZ2JpZklEIjoiNDUwNzk1MzM4NyIsInZlcmJhdGltTG9jYWxpdHkiOiI1UTZGK1dYSCDguJXguLPguJrguKUg4Lia4LmJ4Liy4LiZ4LiB4Lil4Liy4LiHIOC4reC4s+C5gOC4oOC4reC5gOC4oeC4t+C4reC4h+C4meC4hOC4o+C4nuC4meC4oSDguJnguITguKPguJ7guJnguKEgNDgwMDAg4Lib4Lij4Liw4LmA4LiX4Lio4LmE4LiX4LiiIiwiY29sbGVjdGlvbkNvZGUiOiJPYnNlcnZhdGlvbnMiLCJvY2N1cnJlbmNlSUQiOiJodHRwczovL3d3dy5pbmF0dXJhbGlzdC5vcmcvb2JzZXJ2YXRpb25zLzE5NTU1MTU1OSIsInRheG9uSUQiOiI1MTczNyIsImNhdGFsb2dOdW1iZXIiOiIxOTU1NTE1NTkiLCJpbnN0aXR1dGlvbkNvZGUiOiJpTmF0dXJhbGlzdCIsImV2ZW50VGltZSI6IjE0OjMxOjA0KzA3OjAwIiwiaHR0cDovL3Vua25vd24ub3JnL2NhcHRpdmUiOiJ3aWxkIiwiaWRlbnRpZmljYXRpb25JRCI6IjQzOTgyNzUzMiJ9LHsia2V5Ijo0MDExNjM3MjI4LCJkYXRhc2V0S2V5IjoiNTBjOTUwOWQtMjJjNy00YTIyLWE0N2QtOGM0ODQyNWVmNGE3IiwicHVibGlzaGluZ09yZ0tleSI6IjI4ZWIxYTNmLTFjMTUtNGE5NS05MzFhLTRhZjkwZWNiNTc0ZCIsImluc3RhbGxhdGlvbktleSI6Ijk5NzQ0OGE4LWY3NjItMTFlMS1hNDM5LTAwMTQ1ZWI0NWU5YSIsImhvc3RpbmdPcmdhbml6YXRpb25LZXkiOiIyOGViMWEzZi0xYzE1LTRhOTUtOTMxYS00YWY5MGVjYjU3NGQiLCJwdWJsaXNoaW5nQ291bnRyeSI6IkVTIiwicHJvdG9jb2wiOiJEV0NfQVJDSElWRSIsImxhc3RDcmF3bGVkIjoiMjAyNC0wMS0wOVQwMzozMzozOC43MzUrMDA6MDAiLCJsYXN0UGFyc2VkIjoiMjAyNC0wMS0wOVQxMzoxOToyMy4yMTArMDA6MDAiLCJjcmF3bElkIjo0MjYsImV4dGVuc2lvbnMiOnsiaHR0cDovL3JzLmdiaWYub3JnL3Rlcm1zLzEuMC9NdWx0aW1lZGlhIjpbeyJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRlZCI6IjIwMjMtMDEtMDJUMDk6MzU6MzMtMDg6MDAiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvbGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLzQuMC8iLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvdHlwZSI6IlN0aWxsSW1hZ2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvZm9ybWF0IjoiaW1hZ2UvanBlZyIsImh0dHA6Ly9ycy50ZHdnLm9yZy9kd2MvdGVybXMvY2F0YWxvZ051bWJlciI6IjI1MDIzMTQyOCIsImh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9yZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL3Bob3Rvcy8yNTAyMzE0MjgiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmlnaHRzSG9sZGVyIjoiZmFsdWtlIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2NyZWF0b3IiOiJmYWx1a2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzI1MDIzMTQyOC9vcmlnaW5hbC5qcGVnIn0seyJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRlZCI6IjIwMjMtMDEtMDJUMDk6MzU6MTktMDg6MDAiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvbGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLzQuMC8iLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvdHlwZSI6IlN0aWxsSW1hZ2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvZm9ybWF0IjoiaW1hZ2UvanBlZyIsImh0dHA6Ly9ycy50ZHdnLm9yZy9kd2MvdGVybXMvY2F0YWxvZ051bWJlciI6IjI1MDIzMTQ3NCIsImh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9yZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL3Bob3Rvcy8yNTAyMzE0NzQiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmlnaHRzSG9sZGVyIjoiZmFsdWtlIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2NyZWF0b3IiOiJmYWx1a2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzI1MDIzMTQ3NC9vcmlnaW5hbC5qcGVnIn1dfSwiYmFzaXNPZlJlY29yZCI6IkhVTUFOX09CU0VSVkFUSU9OIiwib2NjdXJyZW5jZVN0YXR1cyI6IlBSRVNFTlQiLCJ0YXhvbktleSI6MjkzMDEzNywia2luZ2RvbUtleSI6NiwicGh5bHVtS2V5Ijo3NzA3NzI4LCJjbGFzc0tleSI6MjIwLCJvcmRlcktleSI6MTE3NiwiZmFtaWx5S2V5Ijo3NzE3LCJnZW51c0tleSI6MjkyODk5Nywic3BlY2llc0tleSI6MjkzMDEzNywiYWNjZXB0ZWRUYXhvbktleSI6MjkzMDEzNywic2NpZW50aWZpY05hbWUiOiJTb2xhbnVtIGx5Y29wZXJzaWN1bSBMLiIsImFjY2VwdGVkU2NpZW50aWZpY05hbWUiOiJTb2xhbnVtIGx5Y29wZXJzaWN1bSBMLiIsImtpbmdkb20iOiJQbGFudGFlIiwicGh5bHVtIjoiVHJhY2hlb3BoeXRhIiwib3JkZXIiOiJTb2xhbmFsZXMiLCJmYW1pbHkiOiJTb2xhbmFjZWFlIiwiZ2VudXMiOiJTb2xhbnVtIiwic3BlY2llcyI6IlNvbGFudW0gbHljb3BlcnNpY3VtIiwiZ2VuZXJpY05hbWUiOiJTb2xhbnVtIiwic3BlY2lmaWNFcGl0aGV0IjoibHljb3BlcnNpY3VtIiwidGF4b25SYW5rIjoiU1BFQ0lFUyIsInRheG9ub21pY1N0YXR1cyI6IkFDQ0VQVEVEIiwiaXVjblJlZExpc3RDYXRlZ29yeSI6Ik5FIiwiZGF0ZUlkZW50aWZpZWQiOiIyMDIzLTAxLTAyVDEwOjA4OjIxIiwiZGVjaW1hbExhdGl0dWRlIjozNi44MDc2ODUsImRlY2ltYWxMb25naXR1ZGUiOi0yLjY1ODM2NSwiY29vcmRpbmF0ZVVuY2VydGFpbnR5SW5NZXRlcnMiOjIuMCwiY29udGluZW50IjoiRVVST1BFIiwic3RhdGVQcm92aW5jZSI6IkFuZGFsdWPDrWEiLCJnYWRtIjp7ImxldmVsMCI6eyJnaWQiOiJFU1AiLCJuYW1lIjoiU3BhaW4ifSwibGV2ZWwxIjp7ImdpZCI6IkVTUC4xXzEiLCJuYW1lIjoiQW5kYWx1Y8OtYSJ9LCJsZXZlbDIiOnsiZ2lkIjoiRVNQLjEuMV8xIiwibmFtZSI6IkFsbWVyw61hIn0sImxldmVsMyI6eyJnaWQiOiJFU1AuMS4xLjZfMSIsIm5hbWUiOiJuLmEuICgyMCkifX0sInllYXIiOjIwMjMsIm1vbnRoIjoxLCJkYXkiOjIsImV2ZW50RGF0ZSI6IjIwMjMtMDEtMDJUMDk6MzU6MDAiLCJpc3N1ZXMiOlsiQ09PUkRJTkFURV9ST1VOREVEIiwiQ09OVElORU5UX0RFUklWRURfRlJPTV9DT09SRElOQVRFUyIsIlRBWE9OX01BVENIX1RBWE9OX0lEX0lHTk9SRUQiXSwibW9kaWZpZWQiOiIyMDIzLTAzLTE3VDIyOjE2OjE5LjAwMCswMDowMCIsImxhc3RJbnRlcnByZXRlZCI6IjIwMjQtMDEtMDlUMTM6MTk6MjMuMjEwKzAwOjAwIiwicmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9vYnNlcnZhdGlvbnMvMTQ1NjUzMjQ2IiwibGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLzQuMC9sZWdhbGNvZGUiLCJpZGVudGlmaWVycyI6W3siaWRlbnRpZmllciI6IjE0NTY1MzI0NiJ9XSwibWVkaWEiOlt7InR5cGUiOiJTdGlsbEltYWdlIiwiZm9ybWF0IjoiaW1hZ2UvanBlZyIsInJlZmVyZW5jZXMiOiJodHRwczovL3d3dy5pbmF0dXJhbGlzdC5vcmcvcGhvdG9zLzI1MDIzMTQ3NCIsImNyZWF0ZWQiOiIyMDIzLTAxLTAyVDE3OjM1OjE5LjAwMCswMDowMCIsImNyZWF0b3IiOiJmYWx1a2UiLCJwdWJsaXNoZXIiOiJpTmF0dXJhbGlzdCIsImxpY2Vuc2UiOiJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS1uYy80LjAvIiwicmlnaHRzSG9sZGVyIjoiZmFsdWtlIiwiaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzI1MDIzMTQ3NC9vcmlnaW5hbC5qcGVnIn0seyJ0eXBlIjoiU3RpbGxJbWFnZSIsImZvcm1hdCI6ImltYWdlL2pwZWciLCJyZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL3Bob3Rvcy8yNTAyMzE0MjgiLCJjcmVhdGVkIjoiMjAyMy0wMS0wMlQxNzozNTozMy4wMDArMDA6MDAiLCJjcmVhdG9yIjoiZmFsdWtlIiwicHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJsaWNlbnNlIjoiaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMvNC4wLyIsInJpZ2h0c0hvbGRlciI6ImZhbHVrZSIsImlkZW50aWZpZXIiOiJodHRwczovL2luYXR1cmFsaXN0LW9wZW4tZGF0YS5zMy5hbWF6b25hd3MuY29tL3Bob3Rvcy8yNTAyMzE0Mjgvb3JpZ2luYWwuanBlZyJ9XSwiZmFjdHMiOltdLCJyZWxhdGlvbnMiOltdLCJpc0luQ2x1c3RlciI6ZmFsc2UsImRhdGFzZXROYW1lIjoiaU5hdHVyYWxpc3QgcmVzZWFyY2gtZ3JhZGUgb2JzZXJ2YXRpb25zIiwicmVjb3JkZWRCeSI6ImZhbHVrZSIsImlkZW50aWZpZWRCeSI6ImZhbHVrZSIsImdlb2RldGljRGF0dW0iOiJXR1M4NCIsImNsYXNzIjoiTWFnbm9saW9wc2lkYSIsImNvdW50cnlDb2RlIjoiRVMiLCJyZWNvcmRlZEJ5SURzIjpbXSwiaWRlbnRpZmllZEJ5SURzIjpbXSwiY291bnRyeSI6IlNwYWluIiwicmlnaHRzSG9sZGVyIjoiZmFsdWtlIiwiaWRlbnRpZmllciI6IjE0NTY1MzI0NiIsImh0dHA6Ly91bmtub3duLm9yZy9uaWNrIjoiZmFsdWtlIiwidmVyYmF0aW1FdmVudERhdGUiOiIyMDIzLzAxLzAyIDk6MzUgQU0iLCJnYmlmSUQiOiI0MDExNjM3MjI4IiwidmVyYmF0aW1Mb2NhbGl0eSI6IkFsbWVyw61hLCBFc3Bhw7FhIiwiY29sbGVjdGlvbkNvZGUiOiJPYnNlcnZhdGlvbnMiLCJvY2N1cnJlbmNlSUQiOiJodHRwczovL3d3dy5pbmF0dXJhbGlzdC5vcmcvb2JzZXJ2YXRpb25zLzE0NTY1MzI0NiIsInRheG9uSUQiOiI1MTczNyIsImNhdGFsb2dOdW1iZXIiOiIxNDU2NTMyNDYiLCJpbnN0aXR1dGlvbkNvZGUiOiJpTmF0dXJhbGlzdCIsImV2ZW50VGltZSI6IjA5OjM1OjAwKzAxOjAwIiwicmVwcm9kdWN0aXZlQ29uZGl0aW9uIjoiZnJ1aXRpbmciLCJodHRwOi8vdW5rbm93bi5vcmcvY2FwdGl2ZSI6IndpbGQiLCJpZGVudGlmaWNhdGlvbklEIjoiMzI0MzQ3MTEyIn1dLCJmYWNldHMiOltdfQ== + recorded_at: Sun, 14 Jan 2024 10:17:50 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/cassettes/GbifService/_update_crop/resolves_common_names.yml b/spec/cassettes/GbifService/_update_crop/resolves_common_names.yml new file mode 100644 index 000000000..6a614e43e --- /dev/null +++ b/spec/cassettes/GbifService/_update_crop/resolves_common_names.yml @@ -0,0 +1,159 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.gbif.org/v1/species/search?facet=false&higherTaxonKey=6&hl=false&limit=100&q=Habanero&verbose=false + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday/v1.10.3 Gbif/v0.2.0 + X-USER-AGENT: + - Faraday/v1.10.3 Gbif/v0.2.0 + response: + status: + code: 200 + message: OK + headers: + vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + x-content-type-options: + - nosniff + x-xss-protection: + - 1; mode=block + pragma: + - no-cache + expires: + - '0' + x-frame-options: + - DENY + content-type: + - application/json + date: + - Sun, 14 Jan 2024 10:17:51 GMT + cache-control: + - public, max-age=600 + x-varnish: + - '1003913872' + age: + - '0' + via: + - 1.1 varnish (Varnish/6.0) + accept-ranges: + - bytes + transfer-encoding: + - chunked + connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: !binary |- + eyJvZmZzZXQiOjAsImxpbWl0IjoxMDAsImVuZE9mUmVjb3JkcyI6dHJ1ZSwiY291bnQiOjEsInJlc3VsdHMiOlt7ImtleSI6MjkzMjk0MiwibmFtZUtleSI6MTk3MDM0NywiZGF0YXNldEtleSI6ImQ3ZGRkYmY0LTJjZjAtNGYzOS05YjJhLWJiMDk5Y2FhZTM2YyIsImNvbnN0aXR1ZW50S2V5IjoiN2RkZjc1NGYtZDE5My00Y2M5LWIzNTEtOTk5MDY3NTRhMDNiIiwibnViS2V5IjoyOTMyOTQyLCJwYXJlbnRLZXkiOjI5MzI5MzcsInBhcmVudCI6IkNhcHNpY3VtIiwia2luZ2RvbSI6IlBsYW50YWUiLCJwaHlsdW0iOiJUcmFjaGVvcGh5dGEiLCJvcmRlciI6IlNvbGFuYWxlcyIsImZhbWlseSI6IlNvbGFuYWNlYWUiLCJnZW51cyI6IkNhcHNpY3VtIiwic3BlY2llcyI6IkNhcHNpY3VtIGNoaW5lbnNlIiwia2luZ2RvbUtleSI6NiwicGh5bHVtS2V5Ijo3NzA3NzI4LCJjbGFzc0tleSI6MjIwLCJvcmRlcktleSI6MTE3NiwiZmFtaWx5S2V5Ijo3NzE3LCJnZW51c0tleSI6MjkzMjkzNywic3BlY2llc0tleSI6MjkzMjk0Miwic2NpZW50aWZpY05hbWUiOiJDYXBzaWN1bSBjaGluZW5zZSBKYWNxLiIsImNhbm9uaWNhbE5hbWUiOiJDYXBzaWN1bSBjaGluZW5zZSIsImF1dGhvcnNoaXAiOiJKYWNxLiIsInB1Ymxpc2hlZEluIjoiSmFjcS4gKDE3NzYpLiBJbjogSG9ydC4gQm90LiBWaW5kb2Jvbi4gMzogMzgsIFQuIDY3LiIsIm5hbWVUeXBlIjoiU0NJRU5USUZJQyIsInRheG9ub21pY1N0YXR1cyI6IkFDQ0VQVEVEIiwicmFuayI6IlNQRUNJRVMiLCJvcmlnaW4iOiJTT1VSQ0UiLCJudW1EZXNjZW5kYW50cyI6MCwibnVtT2NjdXJyZW5jZXMiOjAsImV4dGluY3QiOmZhbHNlLCJoYWJpdGF0cyI6WyJURVJSRVNUUklBTCIsIlRFUlJFU1RSSUFMIl0sIm5vbWVuY2xhdHVyYWxTdGF0dXMiOltdLCJ0aHJlYXRTdGF0dXNlcyI6WyJOT1RfRVZBTFVBVEVEIl0sImRlc2NyaXB0aW9ucyI6W3siZGVzY3JpcHRpb24iOiJEZXNjcmlwdGlvbi4gTG93LCBlcmVjdCwgc2hvcnQtbGl2ZWQgc3Vic2hydWJzIG9yIHJhcmVseSBzaHJ1YnMsIDAuNSAtIDEuNSAoLSAyLjUpIG0gdGFsbCwgd2l0aCB0aGUgbWFpbiBzdGVtICgwLjUgLSkgMC44IC0gMS41IGNtIGluIGRpYW1ldGVyIGF0IGJhc2UsIGZldyB0byBtdWNoIGJyYW5jaGVkIGZyb20gbmVhciB0aGUgYmFzZS4gWW91bmcgc3RlbXMgNCAtIGFuZ2xlZCwgZnJhZ2lsZSwgZ3JlZW4gb3IgZ3JlZW5pc2gtYnJvd24sIGdsYWJyb3VzIHRvIGdsYWJyZXNjZW50LCB3aXRoIHNwcmVhZGluZywgc2ltcGxlLCB1bmlzZXJpYXRlLCAoMiAtKSA0IC0gNSAtIGNlbGxlZCwgZWdsYW5kdWxhciB0cmljaG9tZXMgMC4wNyAtIDAuNyBtbSBsb25nOyBub2RlcyBzb2xpZCwgZ3JlZW4gb3IgcHVycGxlOyBiYXJrIG9mIG9sZGVyIHN0ZW1zIGxpZ2h0IGJyb3duIG9yIGdyZWVuIHdpdGggbGlnaHQgYnJvd24gc3RyaXBlcywgZ2xhYnJvdXMgdG8gc3BhcnNlbHkgcHViZXNjZW50OyBsZW50aWNlbHMgYWJzZW50LiBTeW1wb2RpYWwgdW5pdHMgZGlmb2xpYXRlLCB0aGUgbGVhdmVzIGdlbWluYXRlOyBsZWFmIHBhaXIgdW5lcXVhbCBpbiBzaXplLCBzaW1pbGFyIGluIHNoYXBlLiBMZWF2ZXMgbWVtYnJhbm91cywgY29uY29sb3JvdXMgb3Igc2xpZ2h0bHkgZGlzY29sb3JvdXMsIGRhcmsgZ3JlZW4gYWJvdmUsIHBhbGUgZ3JlZW4gYmVsb3csIGdsYWJyZXNjZW50IHRvIHNwYXJzZWx5IHB1YmVzY2VudCwgd2l0aCBzaW1wbGUsIDQgLSA5IC0gY2VsbGVkLCBlZ2xhbmR1bGFyIHRyaWNob21lcyAwLjYgLSAxLjIgbW0gbG9uZywgZXNwZWNpYWxseSBhbG9uZyB0aGUgbWlkLXZlaW4gb3Igd2l0aCBhIHR1ZnQgb2YgdHJpY2hvbWVzIGluIHRoZSBiYXNhbCB2ZWluIGF4aWxzIGFiYXhpYWxseTsgYmxhZGVzIG9mIG1ham9yIGxlYXZlcyAoNCAtKSA1LjI1IC0gMTAgKC0gMTUuNSkgY20gbG9uZywgMi40IC0gNCAoLSA3KSBjbSB3aWRlLCBvdmF0ZSB0byBlbGxpcHRpYywgdGhlIG1ham9yIHZlaW5zIDQgLSA1ICgtIDYpIG9uIGVhY2ggc2lkZSBvZiBtaWQtdmVpbiwgdGhlIGJhc2UgYXR0ZW51YXRlIG9yIHRydW5jYXRlIGFuZCByYXRoZXIgdW5lcXVhbCwgdGhlIG1hcmdpbnMgZW50aXJlLCB0aGUgYXBleCBzaG9ydC1hY3VtaW5hdGUsIGFjdW1pbmF0ZSBvciBhY3V0ZTsgcGV0aW9sZXMgMC41IC0gMyAoLSAzLjUpIGNtLCBnbGFicm91cyB0byBzcGFyc2VseSBwdWJlc2NlbnQ7IGJsYWRlcyBvZiBtaW5vciBsZWF2ZXMgc2ltaWxhciBpbiBzaGFwZSwgNCAtIDUuNSBjbSBsb25nLCAyIC0gMi41IGNtIHdpZGU7IHBldGlvbGVzIDAuNyAtIDEuNSBjbSBsb25nLCB3aXRoIHRoZSBzYW1lIHB1YmVzY2VuY2UgYXMgdGhlIG1ham9yIGxlYXZlcy4gSW5mbG9yZXNjZW5jZXMgYXhpbGxhcnksIDIgLSA0ICgtIDUpIGZsb3dlcnMgcGVyIGF4aWwsIG9jY2FzaW9uYWxseSBzb2xpdGFyeSBmbG93ZXI7IGZsb3dlcmluZyBwZWRpY2VscyAxMiAtIDIwICgtIDMwKSBtbSBsb25nLCBhbmdsZWQsIGVyZWN0IG9yIHNsaWdodGx5IHNwcmVhZGluZywgZ2VuaWN1bGF0ZSBhdCBhbnRoZXNpcyAod2lsZCBmb3Jtcykgb3IgcGVuZGVudCBhbmQgbm9uLWdlbmljdWxhdGUgKGRvbWVzdGljYXRlZCBmb3JtcyksIGdyZWVuIG9yIGdyZWVuIHdpdGggcHVycGxlIGxpbmVzLCBnbGFicm91cyB0byBtb2RlcmF0ZWx5IHB1YmVzY2VudCwgdGhlIGVnbGFuZHVsYXIgdHJpY2hvbWVzIHNob3J0LCBhbnRyb3JzZTsgcGVkaWNlbHMgc2NhcnMgY29uc3BpY3VvdXMsIHNsaWdodGx5IGNvcmt5LiBCdWRzIGVsbGlwc29pZCwgY3JlYW0gb3IgZ3JlZW5pc2gtd2hpdGUuIEZsb3dlcnMgNSAtIDYgLSBtZXJvdXMuIENhbHl4IDEgLSAyLjUgKC0gMykgbW0gbG9uZywgMyAtIDQgbW0gd2lkZSwgY3VwLXNoYXBlZCwgZ3JlZW4sIHBlbnRhZ29uYWwgb3IgaGV4YWdvbmFsIGluIG91dGxpbmUsIHRoZSBtYWluIHZlaW5zIHN0cm9uZ2x5IG1hcmtlZCwgdGhlIGNhbHl4IGFwcGVuZGFnZXMgYWJzZW50IG9yIHdpdGggNSAtIDYgbXVjcm8tbGlrZSBhcHBlbmRhZ2VzLCBnbGFicm91cyB0byBtb2RlcmF0ZWx5IHB1YmVzY2VudCB3aXRoIHNpbWlsYXIgc2hvcnQgb3IgbG9uZyBlZ2xhbmR1bGFyIHRyaWNob21lcyBhcyB0aGUgc3RlbXMuIENvcm9sbGEgNSAoLSA2KSAtIG1lcm91cywgKDUgLSkgNi41IC0gOCBtbSBsb25nLCAxMCAtIDE1ICgtIDIwKSBtbSBpbiBkaWFtZXRlciwgZHVsbCB3aGl0ZSBvciBncmVlbmlzaC13aGl0ZSwgb2NjYXNpb25hbGx5IHdpdGggcHVycGxlIHNwb3RzIG91dHNpZGUgYW5kIHdpdGhpbiwgc3RlbGxhdGUgd2l0aCBpbnRlcnBldGFsYXIgbWVtYnJhbmUsIGxvYmVkIG5lYXJseSAyIC8gMyBvZiB0aGUgd2F5IHRvIHRoZSBiYXNlLCBnbGFicm91cyBhZGF4aWFsbHkgYW5kIGFiYXhpYWxseSwgdGhlIHR1YmUgKDIgLSkgMi41IC0gMyBtbSBsb25nLCB0aGUgbG9iZXMgMy41IC0gNSBtbSBsb25nLCAyIC0gMy41IG1tIHdpZGUsIHRyaWFuZ3VsYXIsIHNwcmVhZGluZywgdGhlIG1hcmdpbnMgcGFwaWxsYXRlLCB0aGUgdGlwcyBhY3V0ZSwgY3VjdWxsYXRlLCBwYXBpbGxhdGUuIFN0YW1lbnMgNSAoLSA2KSwgZXF1YWw7IGZpbGFtZW50cyAxIC0gMS4zIG1tIGxvbmcsIHdoaXRlLCBjcmVhbSBvciBwdXJwbGUsIGluc2VydGVkIG9uIHRoZSBjb3JvbGxhIDEgLSAxLjUgbW0gZnJvbSB0aGUgYmFzZSwgd2l0aCBhdXJpY2xlcyBmdXNlZCB0byB0aGUgY29yb2xsYSBhdCB0aGUgcG9pbnQgb2YgaW5zZXJ0aW9uOyBhbnRoZXJzICgxIC0pIDEuMzggLSAyLjA1IG1tIGxvbmcsIGJsdWUgb3IgYmx1aXNoLWdyZXksIHZlcnkgcmFyZWx5IHllbGxvdyBvciBncmVlbmlzaC13aGl0ZSwgYnJvYWRseSBlbGxpcHNvaWQgb3IgZWxsaXBzb2lkLCBjb25uaXZlbnQgb3Igbm90IGNvbm5pdmVudCBhdCBhbnRoZXNpcywgdGhlIGNvbm5lY3RpdmUgc29tZXRpbWVzIHdpZGUgYW5kIGNsZWFyZXIuIEd5bm9lY2l1bSB3aXRoIHRoZSBvdmFyeSAyIC0gMi41IG1tIGxvbmcsIDIuNSAtIDMuNSBtbSBpbiBkaWFtZXRlciwgMiAtIDMgKC0gNCkgLSBjYXJwZWxhciwgbGlnaHQgZ3JlZW4sIHN1Ymdsb2Jvc2U7IG92dWxlcyBtb3JlIHRoYW4gdHdvIHBlciBsb2N1bGU7IG5lY3RhcnkgY2EuIDAuNSBtbSB0YWxsOyBzdHlsZXMgaGV0ZXJvbW9ycGhpYywgMyAtIDQuMSBtbSwgaW5jbHVkZWQsIGF0IHRoZSBzYW1lIGxldmVsIHRvIHRoZSBzdGFtZW4gbGVuZ3RoIG9yIGV4c2VydGVkIGNhLiAyIG1tIGJleW9uZCB0aGUgYW50aGVycywgbGlsYWMgb3Igd2hpdGUsIGN5bGluZHJpY2FsOyBzdGlnbWEgY2EuIDAuMTUgbW0gbG9uZywgY2EuIDAuNSBtbSB3aWRlLCBtaW51dGUsIGRpc2NvaWQsIHBhbGUgZ3JlZW4gb3IgcGFsZSB5ZWxsb3cuIEJlcnJ5IDwxMCBtbSBpbiBkaWFtZXRlciwgc3ViZ2xvYm9zZSBhbmQgb3JhbmdlIHRvIHJlZCAoaW4gd2lsZCBwb3B1bGF0aW9ucyksIGhpZ2hseSB2YXJpYWJsZSBpbiBzaXplLCBzaGFwZSBhbmQgY29sb3VyIChpbiBzZW1pLWRvbWVzdGljYXRlZCBvciBkb21lc3RpY2F0ZWQgY3VsdGl2YXJzKTogc3ViZ2xvYm9zZSBvciB0cmlhbmd1bGFyLCAxMCAtIDIwIG1tIGluIGRpYW1ldGVyLCB0byBsb25nLXRyaWFuZ3VsYXIgb3IgY2FtcGFudWxhdGUsIDMwIC0gNjAgKC0gMTAwKSBtbSBsb25nLCAyMCAtIDMwIG1tIGluIGRpYW1ldGVyLCBzb21lIGJsb2NreSwgd2l0aCB0aGUgYXBleCBwb2ludGVkLCBibHVudCBvciBsb25nLWFjdW1pbmF0ZSBhbmQgdXBjdXJ2ZWQgYW5kIHRoZSBiYXNlIG9idHVzZSBvciB0cnVuY2F0ZSwgZ3JlZW4sIHllbGxvdywgYnJvd24gb3IgcHVycGxlIHdoZW4gaW1tYXR1cmUsIHBhbGUgeWVsbG93LCB5ZWxsb3csIGRhcmsgYnJvd24sIG9yYW5nZSwgcmVkIG9yIHZlcm1pbGlvbi1zY2FybGV0IGF0IG1hdHVyaXR5LCBkZWNpZHVvdXMgb3IgcGVyc2lzdGVudCwgdmVyeSBwdW5nZW50IChzb21ldGltZXMgbm9uLXB1bmdlbnQpLCB0aGUgcGVyaWNhcnAgdGhpY2ssIG9wYXF1ZSwgd2l0aCBnaWFudCBjZWxscyAoZW5kb2NhcnAgYWx2ZW9sYXRlKTsgc3RvbmUgY2VsbHMgYWJzZW50OyBmcnVpdGluZyBwZWRpY2VscyAxNSAtIDQ1ICgtIDU1KSBtbSBsb25nLCB0aGljaywgYW5nbGVkLCBzdHJvbmdseSB3aWRlbmVkIGRpc3RhbGx5LCBlcmVjdCBhbmQgcmlnaWQgKHdpbGQpIG9yIHBlbmRlbnQgYW5kIGN1cnZlZCAoZG9tZXN0aWNhdGVkKTsgZnJ1aXRpbmcgY2FseXggNSAtIDEwIG1tIGluIGRpYW1ldGVyLCBwZXJzaXN0ZW50LCBub3QgYWNjcmVzY2VudCwgZGlzY29pZCBvciBzaGFsbG93bHkgY3VwLXNoYXBlZCwgc29tZXRpbWVzIHJlZmxleGVkLCB3aXRoIGEgc3Ryb25nIGFubnVsYXIgY29uc3RyaWN0aW9uIGF0IGp1bmN0aW9uIHdpdGggdGhlIHBlZGljZWwgKHdpbGQgYW5kIGRvbWVzdGljYXRlZCksIHNvbWV0aW1lcyB0aGUgbWFyZ2luIHJpcHBlZCwgZ3JlZW4uIFNlZWRzIDE0IC0gMzUgcGVyIGZydWl0LCAoMi43IC0pIDMgLSA0IG1tIGxvbmcsICgyLjUgLSkgMyAtIDMuNSBtbSB3aWRlLCBDLXNoYXBlZCBvciBzdWJnbG9ib3NlLCBwYWxlIHllbGxvdyBvciBuZWFybHkgd2hpdGUsIHRoZSBzZWVkIGNvYXQgc21vb3RoIChTTSksIGNlcmViZWxsb2lkIChTRU0pLCB0aGUgY2VsbHMgaXJyZWd1bGFyIGluIHNlZWQgYm9keSwgcG9seWdvbmFsIGF0IG1hcmdpbnMsIHRoZSBsYXRlcmFsIHdhbGxzIHNpbnVhdGUgaW4gdGhlIHNlZWQgYm9keSwgc3RyYWlnaHQgYXQgbWFyZ2luczsgZW1icnlvIGltYnJpY2F0ZS4ifSx7ImRlc2NyaXB0aW9uIjoiRmlnLiA0NyJ9LHsiZGVzY3JpcHRpb24iOiJEaXNjdXNzaW9uLiBDYXBzaWN1bSBjaGluZW5zZSBiZWxvbmdzIHRvIHRoZSBBbm51bSBjbGFkZSAoQ2Fycml6byBHYXJjaWEgZXQgYWwuIDIwMTYpLiBJdCBpcyBtb3N0IHBvcHVsYXJseSBrbm93biBhcyBIYWJhbmVybyAoTWV4aWNvIGFuZCBVbml0ZWQgU3RhdGVzIG9mIEFtZXJpY2EpIG9yIFNjb3RjaCBCb25uZXQgKENhcmliYmVhbiBJc2xhbmRzKSwgYnV0IGl0IGhhcyBudW1lcm91cyBjb21tb24gYW5kIGluZGlnZW5vdXMgbmFtZXMgaW4gU291dGggQW1lcmljYSAoc2VlIGFib3ZlIGFuZCBCYWJhIGV0IGFsLiAyMDE2KSwgYXMgd2VsbCBhcyBsYW5kcmFjZSBhbmQgY3VsdGl2YXIgbmFtZXMgKGUuIGcuIENhcm9saW5hIFJlYXBlciwgQmh1dCBKb2xva2lhLCBSZWQgU2F2aW5hLCBDb25nbyBwZXBwZXIsIE51TWV4IFN1YXZlLCBOdU1leCBUcmljay1vci1UcmVhdCkuIE1hbnkgb2YgdGhlIG5hbWVzIHJlZmVyIHRvIGRpZmZlcmVuY2VzIGluIHRoZSBmcnVpdCdzIHB1bmdlbmN5LCBzaGFwZSBvciBjb2xvdXIgKEJvc2xhbmQgYW5kIFZvdGF2YSAyMDAwOyBWb3RhdmEgYW5kIEJvc2xhbmQgMjAwNDsgQm9zbGFuZCBhbmQgQmFyYWwgMjAwNzsgQm9zbGFuZCBldCBhbC4gMjAxMjsgQm9zbGFuZCBhbmQgQ29vbiAyMDE1KS4gRXZlciBzaW5jZSBTbWl0aCBhbmQgSGVpc2VyICgxOTU3KSBmaXJzdCByZWNvZ25pc2VkIEMuIGNoaW5lbnNlIGFzIGEgY3VsdGl2YXRlZCBzcGVjaWVzIGluIHRoZSBnZW51cywgaXRzIHJlY29nbml0aW9uIGFzIGFuIGluZGVwZW5kZW50IGVudGl0eSBhbmQgYXMgYSBtZW1iZXIgb2YgdGhlIEMuIGFubnV1bSBjb21wbGV4IChDLiBhbm51dW0gLSBDLiBmcnV0ZXNjZW5zIC0gQy4gY2hpbmVuc2UpIGhhcyBiZWVuIGRlYmF0ZWQgaW4gbGl0ZXJhdHVyZSAoc2VlIEJhcmFsIGFuZCBCb3NsYW5kIDIwMDQ7IEVzaGJhdWdoIDIwMTIgYW5kIHJlZmVyZW5jZXMgdGhlcmVpbiksIHdpdGggYXJndW1lbnRzIGJhc2VkIG9uIG1vcnBob2xvZ2ljYWwsIGN5dG9nZW5ldGljLCBiaW9jaGVtaWNhbCAoaXNvZW56eW1lcyksIHBoeWxvZ2VuZXRpYyBhbmQgcmVwcm9kdWN0aXZlIGV2aWRlbmNlLiBSZWNlbnRseSwgUmF2ZWVuZGFyIGV0IGFsLiAoMjAxNykgZXN0YWJsaXNoZWQgdGhlIHBoeWxvZ2VuZXRpYyByZWxhdGlvbnNoaXBzIG9mIDEzIGNvbXBsZXRlIGNobG9yb3BsYXN0IGdlbm9tZSBzZXF1ZW5jZXMgYmVsb25naW5nIHRvIHRoZSBTb2xhbmFjZWFlIChmaXZlIENhcHNpY3VtIHNwZWNpZXMgaW5jbHVkZWQpLiBUaGlzIHN0dWR5IHNob3dlZCB0aGF0IHRoZSBDLiBjaGluZW5zZSBjcCBnZW5vbWUgaXMgbXVjaCBjbG9zZXIgdG8gQy4gYW5udXVtIHZhci4gZ2xhYnJpdXNjdWx1bSwgd2lsZCBwcm9nZW5pdG9yIG9mIEMuIGFubnV1bSwgdGhhbiBhbnkgb3RoZXIgc2FtcGxlZCBDYXBzaWN1bSBzcGVjaWVzIChDLiBhbm51dW0gdmFyLiBhbm51dW0sIEMuIGZydXRlc2NlbnMsIEMuIGJhY2NhdHVtIGFuZCBDLiBseWNpYW50aG9pZGVzKS4gVGhpcyBpbmRpY2F0ZXMgdGhhdCBtZW1iZXJzIG9mIHRoZSBDLiBhbm51dW0gY29tcGxleCBzaGFyZSBhIGNvbW1vbiBnZW5lIHBvb2wgYXMgd2FzIHN1Z2dlc3RlZCBieSBFc2hiYXVnaCAoMTk4MyksIEliaXphIGV0IGFsLiAoMjAxMikgYW5kIG90aGVycy4gQW4gdW5lcXVpdm9jYWwgbW9ycGhvbG9naWNhbCBkZWxpbWl0YXRpb24gZm9yIEMuIGNoaW5lbnNlIGlzIGRpZmZpY3VsdCBmb3IgdHdvIHJlYXNvbnM6IGZpcnN0LCBpbnRlcm1lZGlhdGUgd2VsbC1lc3RhYmxpc2hlZCB0eXBlcyBvY2N1ciBiZXR3ZWVuIHdpbGQgYW5kIGRvbWVzdGljYXRlZCBmb3JtcywgcHJvYmFibHkgZHVlIHRvIGludHJhc3BlY2lmaWMgaHlicmlkaXNhdGlvbiAoUGlja2Vyc2dpbGwgZXQgYWwuIDE5NzkpIGFuZCBzZWNvbmQsIHNvbWUgc3BlY2ltZW5zIGFyZSBoYXJkbHkgaW5kaXN0aW5ndWlzaGFibGUgZnJvbSB0aGVpciBjbG9zZXN0IHJlbGF0aXZlcyAoQy4gZnJ1dGVzY2VucyBhbmQgQy4gYW5udXVtKSBhdCBmbG93ZXJpbmcgb3IgZnJ1aXRpbmcgc3RhZ2UgKFNtaXRoIGFuZCBIZWlzZXIgMTk1NzsgRXNoYmF1Z2ggMTk3NjsgQmFyYWwgYW5kIEJvc2xhbmQgMjAwNDsgUGlja2Vyc2dpbGwgMjAxNikuIENhcHNpY3VtIGNoaW5lbnNlIGlzIGEgc2hvcnQtbGl2ZWQgc3Vic2hydWIgb3IgcmFyZWx5IHNocnViIHdpdGggMiAtIDQgKC0gNSkgZmxvd2VycyBwZXIgbm9kZSwgZmxvd2VyaW5nIHBlZGljZWxzIHRoYXQgY2FuIGJlIGVyZWN0IG9yIHNwcmVhZGluZyBhbmQgZ2VuaWN1bGF0ZSBhdCBhbnRoZXNpcyAobW9zdGx5IGluIHdpbGQgZm9ybXMpIG9yIHBlbmRlbnQgYW5kIG5vbi1nZW5pY3VsYXRlIChtb3N0bHkgaW4gZG9tZXN0aWNhdGVkIGN1bHRpdmFycyksIGEgY2FseXggd2l0aCBzdHJvbmdseS1tYXJrZWQgbWFpbiB2ZWlucyB0aGF0IGxhY2tzIGFwcGVuZGFnZXMgb3IgaXMgcGVudGFnb25hbCBvciBoZXhhZ29uYWwgaW4gb3V0bGluZSB3aXRoIDUgLSA2IG11Y3JvLWxpa2UgYXBwZW5kYWdlcywgYSBjb3JvbGxhIHRoYXQgaXMgZHVsbCB3aGl0ZSBvciBncmVlbmlzaC13aGl0ZSwgZnJ1aXRzIHRoYXQgYXJlIHZhcmlhYmxlIGluIHNoYXBlLCBzaXplIGFuZCBjb2xvdXIsIGRlY2lkdW91cyBvciBwZXJzaXN0ZW50LCBhIGZydWl0aW5nIGNhbHl4IHdpdGggYSBzdHJvbmcgYW5udWxhciBjb25zdHJpY3Rpb24gYXQgdGhlIGp1bmN0aW9uIHdpdGggdGhlIHBlZGljZWwgYW5kIGEgY2xlYXJseSBkaXNjb2lkIGZydWl0aW5nIGNhbHl4LiBUaGUgZGlzY29pZCBjYWx5eCB3aXRoIGEgZGlzdGluY3QgYW5udWxhciBjb25zdHJpY3Rpb24gaXMgdGhlIG1vc3QgY29uc3BpY3VvdXMgYW5kIGNvbnNpc3RlbnQgZmVhdHVyZSBvZiBDLiBjaGluZW5zZSAoRmlnLiA0NykuIE1vcnBob2xvZ2ljYWxseSwgQmFyYWwgYW5kIEJvc2xhbmQgKDIwMDQpIGZvdW5kIEMuIGNoaW5lbnNlIHByb2R1Y2VzIHByaW1hcmlseSBwZW5kZW50IGZsb3dlcnMgYW5kIGNhbHljZXMgd2l0aCBhIGNpcmN1bGFyIGNvbnN0cmljdGlvbiwgd2hpbGUgQy4gZnJ1dGVzY2VucyBwcm9kdWNlcyBlcmVjdCBmbG93ZXJzIGFuZCBjYWx5Y2VzIHdpdGhvdXQgYSBjb25zdHJpY3Rpb24uIEFub3RoZXIgY2hhcmFjdGVyIGZvdW5kIGluIGxpdGVyYXR1cmUgdG8gZGlzdGluZ3Vpc2ggdGhlc2Ugc3BlY2llcyBpcyB0aGUgZGVncmVlIHRvIHdoaWNoIHRoZSBzdHlsZSBpcyBleHNlcnRlZCBiZXlvbmQgdGhlIGFudGhlcnMgKEQnQXJjeSAxOTczOyBFc2hiYXVnaCAyMDEyKS4gSXQgaXMgbW9zdCBkaWZmaWN1bHQgdG8gZGlzdGluZ3Vpc2ggdGhlc2UgdHdvIHNwZWNpZXMgYXQgdGhlIGZsb3dlcmluZyBzdGFnZS4gVGhlIGdyZWF0IG51bWJlciBvZiBzcGVjaW1lbnMgb2YgQy4gY2hpbmVuc2UsIGFuYWx5c2VkIGluIHRoaXMgdHJlYXRtZW50LCBzaG93ZWQgdGhhdCB0aGUgcG9zaXRpb24gb2YgdGhlIGZsb3dlcmluZyBwZWRpY2VscyBhbmQgd2hldGhlciBvciBub3QgdGhlIHN0eWxlIGlzIGV4c2VydGVkIG1vcmUgdGhhbiAxIG1tIGJleW9uZCB0aGUgYW50aGVycyBjYW4gYmUgcXVpdGUgdmFyaWFibGUgYW5kIHByZXZlbnRzIGNvbmZpZGVudCBzcGVjaWVzIGFzc2lnbm1lbnQgKFBlbmEtWWFtIGV0IGFsLiAyMDA5KS4gQ2FseXggY2hhcmFjdGVycyBjYW4gYmUgaGVscGZ1bCBpbiB0aGUgaWRlbnRpZmljYXRpb24gb2YgZmxvd2VyaW5nIHNwZWNpbWVucy4gVGhlIGZsb3dlcmluZyBjYWx5eCBpbiBib3RoIEMuIGNoaW5lbnNlIGFuZCBDLiBmcnV0ZXNjZW5zIGlzIGRlZXBseSBjdXAtc2hhcGVkLCBidXQgaW4gQy4gY2hpbmVuc2UsIHRoZSBtYWluIG5lcnZlcyBwcm90cnVkZSByZW1hcmthYmx5IGZyb20gdGhlIGNhbHl4IHN1cmZhY2UgKEZpZy4gNDcgQywgSiwgTCksIHdoaWxlIGluIEMuIGZydXRlc2NlbnMsIHRoZXkgYXJlIG9mdGVuIGNvbXBsZXRlbHkgaW1tZXJzZWQgaW4gdGhlIGNhbHl4IHN1cmZhY2UgKEZpZy4gNjggQywgRykgb3IgbWF5IHByb3RydWRlIGRpc3RhbGx5LiBJbiBib3RoIHNwZWNpZXMsIHRoZXNlIG5lcnZlcyBtYXkgc2xpZ2h0bHkgZXhjZWVkIChsZXNzIHRoYW4gMSBtbSkgdGhlIGNhbHl4IGVkZ2UuIEluIGZydWl0LCB0aGVzZSBzcGVjaWVzIGRpZmZlciBjbGVhcmx5LiBDYXBzaWN1bSBjaGluZW5zZSBoYXMgZXJlY3QgYW5kIHVuaWZvcm1seSB3aWRlbmVkIHBlZGljZWxzICh3aWxkIC8gZG9tZXN0aWNhdGVkIGZvcm1zLCBGaWcuIDQ3IEYtSCkgb3IgcGVuZGVudCBwZWRpY2VscyB3aXRoIG9uZSBvciB0d28gc3dlbGxpbmdzIGRpc3RhbGx5IChkb21lc3RpY2F0ZWQgLyBjdWx0aXZhdGVkIGZvcm1zLCBGaWcuIDQ3IEwtSyksIGJ1dCBpbiBib3RoIGNhc2VzLCBhIG5vdGljZWFibGUgY2lyY3VsYXIgY29uc3RyaWN0aW9uIGF0IHRoZSBqdW5jdGlvbiB3aXRoIHRoZSBmcnVpdGluZyBjYWx5eCBpcyBwcmVzZW50LiBJbiBhZGRpdGlvbiwgdGhlIGNhbHl4IGZsYXR0ZW5zIGNvbXBsZXRlbHkgKGRpc2NvaWQgY2FseXgpIGFuZCByZW1haW5zIGFwcHJlc3NlZCBvciByZWZsZXhlZCB0byB0aGUgZnJ1aXQgYmFzZSAoRmlnLiA0NyBILCBNKS4gSW4gY29udHJhc3QsIGluIEMuIGZydXRlc2NlbnMsIHRoZSBwZWRpY2VscyBhcmUgdXN1YWxseSBlcmVjdCBhbmQgd2lkZW4gaW4gYSBzaW1pbGFyIHdheSB0byB0aGUgd2lsZCBmb3JtIG9mIEMuIGNoaW5lbnNlLCBidXQgdGhlIGNpcmN1bGFyIGNvbnN0cmljdGlvbiBpcyB0b3RhbGx5IGFic2VudCBhbmQgdGhlIGZydWl0aW5nIGNhbHl4IHJlbWFpbnMgZGVlcGx5IGN1cC1zaGFwZWQsIGhvdXNpbmcgdGhlIG5hcnJvd2VkIGJhc2Ugb2YgdGhlIGVsb25nYXRlIGZydWl0IChGaWcuIDY4IEcsIEgpLiBUaGUgZGlmZmVyZW50aWF0aW9uIG9mIEMuIGNoaW5lbnNlIGZyb20gQy4gYW5udXVtICh3aWxkIG9yIGRvbWVzdGljYXRlZCBzcGVjaW1lbnMpIGlzIHNvbWV0aW1lcyBhbHNvIGRpZmZpY3VsdCwgZXNwZWNpYWxseSBhdCB0aGUgZnJ1aXRpbmcgc3RhZ2UuIFdpbGQgZm9ybXMgb2YgQy4gY2hpbmVuc2Ugd2l0aCBzbWFsbCByZWQgZnJ1aXRzIGFuZCBlcmVjdCBwZWRpY2VscyBjb3VsZCBiZSBjb25mdXNlZCB3aXRoIHNvbWUgZG9tZXN0aWNhdGVkIChvciBzZW1pLWRvbWVzdGljYXRlZCkgc3BlY2ltZW5zIG9mIEMuIGFubnV1bSB2YXIuIGdsYWJyaXVzY3VsdW0sIHdoaWNoIHNoYXJlIHRoZSBzYW1lIHBlZGljZWwgcG9zaXRpb24gYW5kIGZydWl0IGNoYXJhY3RlcmlzdGljcyAoc2l6ZSwgc2hhcGUgYW5kIGNvbG91ciksIGFsdGhvdWdoIHdpbGQgZm9ybXMgb2YgdGhpcyB2YXJpZXR5IGhhdmUgc21hbGxlciBmcnVpdHMgdGhhbiB0aGUgdHlwaWNhbCBDLiBjaGluZW5zZS4gVGhlIHByZXNlbmNlIC8gYWJzZW5jZSBvZiB0aGUgY29uc3RyaWN0aW9uIGluIHRoZSBwZWRpY2VsIC8gY2FseXgganVuY3Rpb24gKGxhY2tpbmcgaW4gQy4gYW5udXVtIHZhci4gZ2xhYnJpdXNjdWx1bSkgYWxsb3dzIHRoZSBhc3NpZ25tZW50IHRvIG9uZSBvciBvdGhlciB0YXhvbi4gRnVydGhlcm1vcmUsIEMuIGNoaW5lbnNlIGlzIGEgbW9yZSByb2J1c3QgcGxhbnQgd2l0aCBsYXJnZXIgbGVhdmVzIGFuZCBmbG93ZXJzIHRoYW4gQy4gYW5udXVtIHZhci4gZ2xhYnJpdXNjdWx1bS4gRHVyaW5nIGl0cyBsb25nIGhpc3Rvcnkgb2YgY3VsdGl2YXRpb24sIEMuIGNoaW5lbnNlIGZydWl0cyBoYXZlIGRldmVsb3BlZCBhIGJyb2FkIHJhbmdlIG9mIHZhcmlhdGlvbiBkdWUgdG8gYWN0aXZlIHNlbGVjdGlvbiBieSBncm93ZXJzLCBkaWZmZXJpbmcgY3VsdGl2YXRpb24gbWV0aG9kcyBhbmQgYWRhcHRhdGlvbiB0byB0aGUgZW52aXJvbm1lbnQgKEJhcmJvc2EgZXQgYWwuIDIwMTApOyBpbiBhZGRpdGlvbiwgdGhlcmUgbWF5IGhhdmUgYmVlbiBoaXN0b3JpY2FsIGFuZCBnZW9ncmFwaGljYWwgaXNvbGF0aW9uIG9mIHN1YnBvcHVsYXRpb25zIChCaGFyYXRoIGV0IGFsLiAyMDEzKS4gVGhpcyB2YXJpYXRpb24gaGFzIGJlZW4gb2JzZXJ2ZWQgYWxvbmcgaXRzIGRpc3RyaWJ1dGlvbiAoQmhhcmF0aCBldCBhbC4gMjAxMzsgQmFiYSBldCBhbC4gMjAxNjsgTW9yZWlyYSBldCBhbC4gMjAxOCksIHdoZXJlIGVsb25nYXRlLCBjYW1wYW51bGF0ZSBhbmQgYmxvY2t5IGZydWl0IHNoYXBlcyBhcmUgbW9yZSBwcmVkb21pbmFudCB0aGFuIHRyaWFuZ3VsYXIgb3Igcm91bmRlZCBzaGFwZXMuIFNpbWlsYXJseSwgcmVkIGZydWl0cywgZm9sbG93ZWQgYnkgb3JhbmdlIG9yIHllbGxvdyBvbmVzLCBhcmUgdGhlIG1vc3QgY29tbW9uLCB3aXRoIHJhcmUgb2NjdXJyZW5jZXMgb2Ygd2hpdGUsIG9yYW5nZS15ZWxsb3csIGJsYWNrIG9yIGJyb3duIGZydWl0cy4gVGhlIHB1bmdlbmN5IGFuZCBhcm9tYSBvZiB0aGlzIHNwZWNpZXMgYXJlIGFsc28gcmVtYXJrYWJsZS4gVGhlIFwiIENhcm9saW5hIFJlYXBlciBcIiBjdWx0aXZhciBpcyByZXB1dGVkIHRvIGJlIHRoZSB3b3JsZCdzIGhvdHRlc3QgY2hpbGUsIHdoaWNoIHJhdGVzIGF0IGFuIGF2ZXJhZ2Ugb2YgMSw2NDEsMTgzIFNjb3ZpbGxlIEhlYXQgVW5pdHMgKFNIVSkgKGh0dHBzOiAvLyB3d3cuIGd1aW5uZXNzd29ybGRyZWNvcmRzLiBjb20gLyB3b3JsZC1yZWNvcmRzIC8gaG90dGVzdC1jaGlsaSwgYWNjZXNzZWQgb24gMjkgT2N0b2JlciAyMDE5KSBhbmQgaXMgYSBmYXZvdXJpdGUgYW1vbmdzdCBob3QtcGVwcGVyIGxvdmVycy4gQXQgdGhlIG90aGVyIGV4dHJlbWUsIHRoZSBmcnVpdHMgb2YgdGhlIGxvdyBwdW5nZW5jeSB2YXJpZXRpZXMgKHBpbWVudGEtZG9jZSwgcGltZW50YS1kZS1jaGVpcm8gbG9uZ2EsIHBpbWVudGEtZGUtY2hlaXJvIGFtYXJlbGEpIGZvdW5kIGluIEJyYXppbCAoUm9yYWltYSkgYXJlIHVzZWQgYnkgaW5kaWdlbm91cyBhbmQgbm9uLWluZGlnZW5vdXMgY29tbXVuaXRpZXMgYW5kIGFyZSBwcmVmZXJlbnRpYWxseSB1c2VkIGluIHN0ZXdzIG9yIHNhbGFkcywgZm9yIHByZXBhcmluZyBqZWxsaWVzIG9yIGxpcXVldXJzIG9yIGZvciBmb29kIGRpc2ggZGVjb3JhdGlvbiAoQmFyYm9zYSBldCBhbC4gMjAwNikuIFRoZSBmcnVpdHMgYXJlIGFsc28gZGlzdGluZ3Vpc2hlZCBieSB0aGVpciBkaXN0aW5jdCB2b2xhdGlsZSBwcm9maWxlcyAoZGVwZW5kaW5nIG9uIHRoZSBjdWx0aXZhcnMpLCB3aGljaCBnaXZlIHRoZW0gYSBwb3dlcmZ1bCBleG90aWMtZnJ1aXR5IGFyb21hIChSb2RyaWd1ZXotQnVycnVlem8gZXQgYWwuIDIwMTA7IEdhcnJ1dGkgZXQgYWwuIDIwMTMgYW5kIHJlZmVyZW5jZXMgdGhlcmVpbjsgUGF0ZWwgZXQgYWwuIDIwMTYpLiBCYXJhbCBhbmQgQm9zbGFuZCAoMjAwNCkgcHJvdmlkZWQgbW9ycGhvbG9naWNhbCwgcGh5bG9nZW5ldGljIChSQURQKSBhbmQgcmVwcm9kdWN0aXZlIChzZXh1YWwgY29tcGF0aWJpbGl0eSkgZXZpZGVuY2UgdG8gZGlmZmVyZW50aWF0ZSBDLiBjaGluZW5zZSBmcm9tIEMuIGZydXRlc2NlbnMsIHdoaWxlIGEgc3R1ZHkgb2YgZ2VuZXRpYyBkaXZlcnNpdHkgKFNTUiBhbmQgQUZMUCkgKEliaXphIGV0IGFsLiAyMDEyKSBoYXMgc2hvd24gdGhhdCwgYWx0aG91Z2ggQy4gY2hpbmVuc2UgYW5kIEMuIGZydXRlc2NlbnMgYXJlIGNsb3NlbHkgcmVsYXRlZCwgdGhlIG1vbGVjdWxhciBjaGFyYWN0ZXJpc2F0aW9uIG9idGFpbmVkIGlzIHN1ZmZpY2llbnQgdG8gZGlmZmVyZW50aWF0ZSB0aGUgaW5kaXZpZHVhbCBtZW1iZXJzIG9mIHRoZXNlIHNwZWNpZXMuIEludGVybWVkaWF0ZSBwaGVub3R5cGVzIGFuZCBpbnRlcm1lZGlhdGUgZ2VuZXRpYyBhY2Nlc3Npb25zIHdlcmUgZXhwbGFpbmVkIGJ5IGh5YnJpZGlzYXRpb24sIHNpbmNlIGdlbmUgZmxvdyBjb3VsZCBiZSBvY2N1cnJpbmcgaW4gc3ltcGF0cmljIGFyZWFzIG9mIHRoZXNlIHR3byBzcGVjaWVzIChCYXJhbCBhbmQgQm9zbGFuZCAyMDA0OyBJYml6YSBldCBhbC4gMjAxMikuIEphY3F1aW4gKDE3NzYpIGRlc2NyaWJlZCBDLiBjaGluZW5zZSwgYmFzZWQgb24gY3VsdGl2YXRlZCBtYXRlcmlhbC4gSW4gdGhlIHByb3RvbG9ndWUsIGhlIHN0YXRlZCB0aGF0IGhlIHNhdyB0aGlzIHNwZWNpZXMgY3VsdGl2YXRlZCBvbiB0aGUgSXNsYW5kIG9mIE1hcnRpbmlxdWUgKExlc3NlciBBbnRpbGxlcyksIGEgc21hbGwgaXNsYW5kIGluIHRoZSBDYXJpYmJlYW4gU2VhLCBidXQgaGUgY2FsbGVkIGl0IEMuIGNoaW5lbnNlLCBhbGx1ZGluZyB0byB0aGUgcmVnaW9uIHRvIHdoaWNoIGhlIHRob3VnaHQgdGhlIHBsYW50IHdhcyBuYXRpdmUuIFdlIGZvdW5kIHByb2JhYmxlIG9yaWdpbmFsIG1hdGVyaWFsIG9ubHkgYXQgVyAoVy1hY2MuICMgMDA4MDExNSkuIFRoaXMgc2hlZXQgaGFzIHR3byBsYWJlbHMsIG9uZSBzbWFsbGVyIGF0IHRoZSBsb3dlciBsZWZ0IGNvcm5lciB3aXRoIHRoZSBzY3JpcHQgXCIgQ2Fwc2ljdW0gc2luZW5zZSBcIiwgcHJvYmFibHkgaW4gSmFjcXVpbidzIGZpbGl1cyB3cml0aW5nIChzZWUgRCdBcmN5IDE5NzApIGFuZCBhbm90aGVyIGF0IHRoZSBsb3dlciByaWdodCBjb3JuZXIgd2hlcmUgXCIgQ2Fwc2ljdW0gc2luZW5zZSBcIiBhbmQgXCIgSG9ydC4gYm90LiBWaW5kLiBcIiBhcmUgaGFuZHdyaXR0ZW4gYnkgYW4gdW5rbm93biBoYW5kOyB0aGlzIHNoZWV0IGlzIGFsc28gYW5ub3RhdGVkIFwiIEhiLiBKYWNxLiBcIiBpbiBhIGRpZmZlcmVudCBoYW5kIGFuZCBjbGVhcmVyIGluay4gV2UgZGVzaWduYXRlIHRoaXMgc2hlZXQgKFctYWNjLiAjIDAwODAxMTUpIGFzIHRoZSBsZWN0b3R5cGUgZm9yIHRoaXMgc3BlY2llcy4gQ2Fwc2ljdW0gY2VyYXNpZm9ybWUgd2FzIHB1Ymxpc2hlZCBieSBMYW1hcmNrICgxNzk0KSB3aXRoIG9ubHkgdGhlIHR5cGUgbG9jYWxpdHkgXCIgRSBCcmFmaWxpYSBcIiBjaXRlZC4gQXQgUCwgd2UgZm91bmQgYSBjb2xsZWN0aW9uIGluIExhbWFyY2sncyBIZXJiYXJpdW0gd2l0aCBhIGxhYmVsIGluZGljYXRpbmcgdGhhdCBpdCBiZWxvbmdzIHRvIFwiIENhcHNpY3VtIGNlcmFzaWZvcm1lIFwiIChQIDAwMzU3NzMzKSB3aGljaCB3ZSBkZXNpZ25hdGUgYXMgdGhlIGxlY3RvdHlwZS4gRm9yIEMuIGx1dGV1bSwgTGFtYXJjayAoMTc5NCkgY2l0ZWQgaW4gdGhlIHByb3RvbG9ndWUgXCIgRXggSW5kaWEuIFNvbm5lcmF0IFwiIGFuZCB0aGUgdmVybmFjdWxhciBuYW1lcyAnIHBpbWVudCBqYXVuZScgYW5kICcgTGUgcGltZW50IGRlIE1vemFtYmlxdWUnLiBBdCBQLCB3ZSBmb3VuZCBhIHNoZWV0IGluIExhbWFyY2sncyBIZXJiYXJpdW0gd2l0aCBvcmlnaW5hbCBtYXRlcmlhbCBhbmQgdGhlIHNhbWUgZGF0YSAoUCAwMDM1NzczMSkuIFRoZSBsYWJlbCBoYXMgYSBkZXNjcmlwdGlvbiBhbmQgcGVuY2lsIGRyYXdpbmdzIHRoYXQgbGlrZWx5IHdlcmUgdXNlZCBpbiBwcmVwYXJpbmcgdGhlIGRlc2NyaXB0aW9uLiBXZSBzZWxlY3QgdGhpcyBzaGVldCBhcyB0aGUgbGVjdG90eXBlLiBUaGVyZSBhcmUgdGhyZWUgc2hlZXRzIG9mIG9yaWdpbmFsIG1hdGVyaWFsIGxhYmVsbGVkIEMuIGNlcmFzaWZvcm1lIGluIFdpbGxkZW5vdydzIEhlcmJhcml1bSBoZWxkIGF0IEJlcmxpbi4gQWxsIG9mIHRoZW0gY29udGFpbiByZXByb2R1Y3RpdmUgYnJhbmNoZXM7IG9uZSBvZiB0aGVzZSAoQi1XIDA0NDI3IC0gMDEgLSAwKSBjb25zaXN0cyBvZiBhIGZydWl0aW5nIGJyYW5jaCB0aGF0IG1hdGNoZXMgV2lsbGRlbm93J3MgZGVzY3JpcHRpb24gZXhhY3RseSAoV2lsbGRlbm93IDE4MDkpIGFuZCBpcywgdGhlcmVmb3JlLCBkZXNpZ25hdGVkIHRoZSBsZWN0b3R5cGUuIEluIGRlc2NyaWJpbmcgQy4gdG94aWNhcml1bSwgUG9lcHBpZyAoMTgzMikgY2l0ZWQgbm8gaGVyYmFyaXVtIG1hdGVyaWFsLiBQb2VwcGlnJ3MgSGVyYmFyaXVtIGlzIGhvdXNlZCBhdCBXIChTdGFmbGV1IGFuZCBDb3dhbiAxOTgzKSB3aGVyZSB0d28gc3BlY2ltZW5zIGZyb20gUGVydSB3aXRoIHRoZSBoYW5kd3JpdHRlbiBuYW1lIEMuIHRveGljYXJpdW0gYXJlIGhlbGQuIE9uIG9uZSBvZiB0aGUgc3BlY2ltZW5zIGlzIGEgbm90ZSByZWZlcnJpbmcgdG8gdGhlIHBvaXNvbm91cyBlZmZlY3Qgb2YgdGhpcyBwbGFudCBrbm93biBhcyBcIiBhamkgZGUgdmVuZW5vIFwiLCBhIHZlcm5hY3VsYXIgbmFtZSBjaXRlZCBpbiB0aGUgcHJvdG9sb2d1ZS4gVGhpcyBzaGVldCBjb25zaXN0cyBvZiBmb3VyIGZydWl0aW5nIGJyYW5jaGVzIGFuZCBhIGZpZnRoIHN0ZXJpbGUgYnJhbmNoIChhY2MuICMgMDEwMjE5NikuIFRoZSBzZWNvbmQgc2hlZXQgaXMgYSBwb29yZXIgc3BlY2ltZW4gKG9uZSBicmFuY2ggd2l0aCBvbmx5IG9uZSBmcnVpdCwgYWNjLiAjIDAxMDIxOTUpIGFuZCBoYXMgYSBkaWZmZXJlbnQgY29sbGVjdGlvbiBkYXRlIGZyb20gdGhlIG90aGVyIHNoZWV0LiBXZSBkZXNpZ25hdGUgVyBhY2MuICMgMDEwMjE5NiBhcyB0aGUgbmVvdHlwZSBvZiBDLiB0b3hpY2FyaXVtLiBCZXJ0b2xvbmkgKDE4MzgpIGRlc2NyaWJlZCBDLiBjZXJlb2x1bSwgYmFzZWQgb24gbWF0ZXJpYWwgY3VsdGl2YXRlZCBpbiB0aGUgQm90YW5pY2FsIEdhcmRlbiBvZiBCb2xvZ25hIChJdGFseSksIGZyb20gQnJhemlsaWFuIHNlZWRzLiBXZSBjb3VsZCBmaW5kIG5vIHNwZWNpbWVucywgYnV0IEJlcnRvbG9uaSBwcm92aWRlZCBhIGNvbG91ciBwbGF0ZSBmb3IgQy4gY2VyZW9sdW0gdGhhdCBjb25zaXN0cyBvZiBhIGZsb3dlcmluZyBhbmQgYSBmcnVpdGluZyBicmFuY2g7IGJvdGggZGVzY3JpcHRpb24gYW5kIHBsYXRlIG1hdGNoIHdpdGggb3VyIGNvbmNlcHQgb2YgQy4gY2hpbmVuc2UgYW5kIHdlIGRlc2lnbmF0ZSB0aGlzIGlsbHVzdHJhdGlvbiBhcyB0aGUgbGVjdG90eXBlLiBQYXh0b24gKDE4MzgpIGRlc2NyaWJlZCBDLiB1c3R1bGF0dW0sIGJhc2VkIG9uIGN1bHRpdmF0ZWQgcGxhbnRzIGdyb3duIGF0IENoYXRzd29ydGggKEVuZ2xhbmQpIG9idGFpbmVkIGZyb20gc2VlZHMgb2YgdW5rbm93biBvcmlnaW4sIHNlbnQgYnkgSi4gQmF0ZW1hbiwgRXNxLiwgb2YgS255cGVyc2x5LCB1bmRlciB0aGUgbmFtZSBcIiBUcnVlIENoaWxpIENhcHNpY3VtIFwiLiBQYXh0b24gaXMgbm90IGtub3duIHRvIGhhdmUgbWFkZSBoZXJiYXJpdW0gbWF0ZXJpYWwgKFN0YWZsZXUgYW5kIENvd2FuIDE5ODMpLCBidXQgaGUgcHJvdmlkZWQgYSBwbGF0ZSBvcHBvc2l0ZSBwYWdlIDE5NyBpbiBQYXh0b24gKDE4MzgpIGxhYmVsbGVkIG9ubHkgYXMgXCIgQ2Fwc2ljdW0gXCIgYWx0aG91Z2ggY2xlYXJseSBhc3NvY2lhdGVkIHdpdGggdGhlIHByb3RvbG9ndWU7IHRoaXMgcGxhdGUgaXMgaGVyZSBkZXNpZ25hdGVkIGFzIHRoZSBsZWN0b3R5cGUuIEluIHRoZSBwcm90b2xvZ3VlIG9mIEMuIGNlcmFzaWZvcm1lIHZhci4gbWF1cm9jYXJwdW0sIER1bmFsICgxODUyKSBjaXRlZCBubyBzcGVjaW1lbnMsIGJ1dCByZWZlcnJlZCB0aGlzIHZhcmlldHkgdG8gYSBwcmUtTGlubmFlYW4gY29sb3VyIGZpZ3VyZSBwdWJsaXNoZWQgYnkgV2VpbmVtYW5uICgxNzQ1KSwgd2hpY2ggaXMgaGVyZSBzZWxlY3RlZCBhcyB0aGUgbGVjdG90eXBlLiBGcnVpdCBjaGFyYWN0ZXJzIGRlcGljdGVkIG9uIHRoZSBwbGF0ZSBsZWF2ZSBubyBkb3VidCB0aGF0IHRoaXMgdmFyaWV0eSBjb3JyZXNwb25kcyB0byBDLiBjaGluZW5zZS4gRHVuYWwgKDE4NTIpIGJhc2VkIEMuIGNvcmRpZm9ybWUgdmFyLiBzdWJzdWxjYXR1bSBvbiBGaWcuIElYIGMgKGFzIEMuIGNvcmRpZm9ybWUpIHB1Ymxpc2hlZCBieSBGaW5nZXJodXRoICgxODMyKTsgYXMgRHVuYWwgZGlkIG5vdCBtZW50aW9uIGFueSBzcGVjaW1lbnMsIHdlIGRlc2lnbmF0ZSBGaW5nZXJodXRoJ3MgZmlndXJlIGFzIHRoZSBsZWN0b3R5cGUuIFRoZSBvcmlnaW5hbCBtYXRlcmlhbCBvZiBDLiBhc3NhbWljdW0gY291bGQgbm90IGJlIGZvdW5kIGFuZCBpcyBhcHBhcmVudGx5IGxvc3QuIEFjY29yZGluZyB0byB0aGUgcHJvdG9sb2d1ZSAoUHVya2F5YXN0aGEgZXQgYWwuIDIwMTI6IDU2KSBcIiBWb3VjaGVyIHNwZWNpbWVucyBvZiB0aGUgdGF4b24gYXJlIGxvZGdlZCBhdCB0aGUgSGVyYmFyaXVtIG9mIERlZmVuY2UgUmVzZWFyY2ggTGFib3JhdG9yeSAoRFJETyksIFRlenB1ciwgQXNzYW0gKERSTFQsIHVucmVnaXN0ZXJlZCBoZXJiYXJpdW0gYWNyb255bSksIEluZGlhIGFuZCB3YXMgYWxzbyBzZW50IHRvIHRoZSBIZXJiYXJpdW0gb2YgQm90YW5pY2FsIFN1cnZleSBvZiBJbmRpYSwgRWFzdGVybiBDaXJjbGUsIFNoaWxsb25nIChCU0kpLCBJbmRpYSBcIi4gVGhlIGF1dGhvcnMgd2VyZSB3cm9uZyB3aGVuIHRoZXkgYXNzaWduZWQgdGhlIGFjcm9ueW0gQlNJIHRvIHRoZSBFYXN0ZXJuIENpcmNsZSwgU2hpbGxvbmc7IGl0IGlzIHNob3VsZCBiZSBBU1NBTS4gQlNJIHN0YW5kcyBmb3IgQm90YW5pY2FsIFN1cnZleSBvZiBJbmRpYSwgV2VzdGVybiBDaXJjbGUsIFB1bmUsIEluZGlhIChUaGllcnMgMjAyMSkuIEFuIGFubm90YXRpb24gaW4gVGhlIEludGVybmF0aW9uYWwgUGxhbnQgTmFtZSBJbmRleCAoaHR0cHM6IC8vIHd3dy4gaXBuaS4gb3JnIC8gbiAvIDYwNDYwMjgzIC0gMiwgYWNjZXNzZWQgb24gMTMgRGVjZW1iZXIgMjAxOSkgXCIgUHVya2F5YXN0aGEgKHBlcnMuIGNvbW0uKSBhZG1pdHRlZCB0aGF0IHRoZSBjaXRhdGlvbiBvZiBCU0kgd2FzIGFuIGVycm9yIGZvciBDQUwgXCIuIEFmdGVyIGNvbnN1bHRpbmcgdGhlIHJlc3BlY3RpdmUgQ0FMLCBCU0kgYW5kIEFTU0FNIGN1cmF0b3JzIChCYXJib3phIGluIGxpdHQuKSwgbmVpdGhlciBob2xvdHlwZSBub3IgaXNvdHlwZXMgaGF2ZSBiZWVuIGxvY2F0ZWQgaW4gdGhlc2UgSGVyYmFyaWEuIn0seyJkZXNjcmlwdGlvbiI6IkRpc3RyaWJ1dGlvbi4gQ2Fwc2ljdW0gY2hpbmVuc2Ugd2lsZCBmb3JtcyB3ZXJlIHRob3VnaHQgdG8gb3JpZ2luYWxseSBvY2N1ciBpbiB0aGUgbm9ydGgtY2VudHJhbCBBbWF6b24gQmFzaW4gbG93bGFuZHMgKEJyYXppbCksIHdoZXJlIGRvbWVzdGljYXRpb24gaXMgdGhvdWdodCB0byBoYXZlIG9jY3VycmVkIChQaWNrZXJzZ2lsbCAxOTcxOyBFc2hiYXVnaCAxOTkzKS4gUGlja2Vyc2dpbGwgKDE5ODQpIHN0YXRlZCB0aGF0IHdpbGQgQy4gY2hpbmVuc2UgaXMgY29uZmluZWQgdG8gdGhlIGxvd2xhbmRzIG9mIHRoZSBBbWF6b24sIE9yaW5vY28gYW5kIGVhc3Rlcm4gQnJhemlsLCB3aGlsZSBNb3NlcyBldCBhbC4gKDIwMTQpIHN1Z2dlc3RlZCB0aGF0IHBvcHVsYXRpb25zIGluIENlbnRyYWwgQW1lcmljYSBhbmQgdGhlIENhcmliYmVhbiBtYXkgaGF2ZSBiZWVuIHByaW1hcmlseSBkZXJpdmVkIGZyb20gcHJvZ2VuaXRvcnMgZnJvbSB0aGUgVXBwZXIgQW1hem9uIFJlZ2lvbiBhbmQgbGF0ZXIgZGl2ZXJnZWQgdGhyb3VnaCBnZW9ncmFwaGljYWwgaXNvbGF0aW9uLiBXaWxkIHBvcHVsYXRpb25zIGNhbiBzdGlsbCBiZSBmb3VuZCBpbiB0aGUgbmF0dXJlLCBidXQgdGhleSBhcmUgZGlmZmljdWx0IHRvIGxvY2F0ZSBvciBhcmUgcmVzdHJpY3RlZCB0byByZW1vdGUgYXJlYXMuIFdpbGQgQy4gY2hpbmVuc2UgKGluZGlnZW5vdXMgbmFtZTogUGltaSdybykgaGF2ZSBiZWVuIGZvdW5kIGluIFJvcmFpbWEgU3RhdGUgKEJyYXppbCkgKEJhcmJvc2EgZXQgYWwuIDIwMDIsIDIwMDg7IEJ1c3NvIGV0IGFsLiAyMDAzOyBOYXNjaW1lbnRvLUZpbGhvIGV0IGFsLiAyMDA3OyBSb21lcm8gZGEgQ3J1eiBhbmQgRm9ybmkgTWFydGlucyAyMDE1KSwgd2hpY2ggY29uZmlybXMgdGhlIG9jY3VycmVuY2Ugb2YgcHJvYmFibGUgYW5jZXN0b3JzIGluIHRoZSBlYXN0ZXJuIGxvd2xhbmRzIG9mIEFtYXpvbmlhIChGaWcuIDQ4KS4gQ2Fwc2ljdW0gY2hpbmVuc2UgaGFzIGJlZW4gaW50cm9kdWNlZCBpbnRvIFVuaXRlZCBTdGF0ZXMgb2YgQW1lcmljYSAoQm9zbGFuZCBhbmQgVm90YXZhIDIwMDApLCBNZXhpY28gdGhyb3VnaCBDdWJhIChHb256YWxleiBFc3RyYWRhIGV0IGFsLiAyMDEwOyBSdWl6LUxhdSBldCBhbC4gMjAxMTsgTG9wZXogQ2FzdGlsbGEgZXQgYWwuIDIwMTkpIGFuZCBDZW50cmFsIGFuZCBTb3V0aCBBbWVyaWNhIHdoZXJlIGlzIGZvdW5kIGN1bHRpdmF0ZWQgb3IgZXNjYXBlZCBmcm9tIGN1bHRpdmF0aW9uOyBpdCBoYXMgYWxzbyBiZWVuIGludHJvZHVjZWQgb3V0c2lkZSB0aGUgQW1lcmljYXMgKEVhc3Rlcm4gRXVyb3BlLCBBZnJpY2EsIEFzaWE6IENoaW5hLCBKYXBhbiwgVGFpd2FuLCBJbmRpYSBhbmQgb3RoZXJzKSBtYWlubHkgYnkgUG9ydHVndWVzZSBleHBsb3JlcnMgKEVzaGJhdWdoIDE5ODM7IEFuZHJld3MgMTk5MzsgTWVnaHZhbnNpIGV0IGFsLiAyMDEwKS4ifSx7ImRlc2NyaXB0aW9uIjoiU3BlY2ltZW5zIGV4YW1pbmVkLiBTZWUgU3VwcGwuIG1hdGVyaWFsIDQ6IEFwcGVuZGl4IDQuIn0seyJkZXNjcmlwdGlvbiI6IlR5cGUuIEN1bHRpdmF0ZWQgaW4gVmllbm5hLCBBdXN0cmlhIFtcIiBIb3J0LiBCb3QuIFZpbmRvYi4gXCJdLCBOLiBKLiB2b24gSmFjcXVpbiBzLiBuLiAobGVjdG90eXBlLCBkZXNpZ25hdGVkIGhlcmU6IFcgW2FjYy4gIyAwMDgwMTE1XSkuIn0seyJkZXNjcmlwdGlvbiI6IjgwIC0gMTAwMCBtc25tIn0seyJkZXNjcmlwdGlvbiI6IkFtYXpvbmlhLCBHdWF5YW5hIHkgU2VycmFuw61hIGRlIExhIE1hY2FyZW5hLCBJc2xhcyBDYXJpYmXDsWFzIn0seyJkZXNjcmlwdGlvbiI6Ik3DqXhpY28gYSBCcmFzaWwifSx7ImRlc2NyaXB0aW9uIjoiQWxpbWVudGljaW8ifSx7ImRlc2NyaXB0aW9uIjoiQ3VsdGl2YWRhIn0seyJkZXNjcmlwdGlvbiI6IkV4w7N0aWNhIn0seyJkZXNjcmlwdGlvbiI6IlPDrSJ9XSwidmVybmFjdWxhck5hbWVzIjpbeyJ2ZXJuYWN1bGFyTmFtZSI6ImhhdmFubmFwZXBwYXIiLCJsYW5ndWFnZSI6InN3ZSJ9LHsidmVybmFjdWxhck5hbWUiOiJ5ZWxsb3cgc3F1YXNoIHBlcHBlciIsImxhbmd1YWdlIjoiZW5nIn0seyJ2ZXJuYWN1bGFyTmFtZSI6ImJvbm5ldCBwZXBwZXIiLCJsYW5ndWFnZSI6ImVuZyJ9LHsidmVybmFjdWxhck5hbWUiOiJwaW1lbnRhLWRlLWNoZWlybyJ9LHsidmVybmFjdWxhck5hbWUiOiJkYXRpbCBwZXBwZXIiLCJsYW5ndWFnZSI6ImVuZyJ9LHsidmVybmFjdWxhck5hbWUiOiJwaXJpLXBpcmkgcGVwcGVyIiwibGFuZ3VhZ2UiOiJlbmcifSx7InZlcm5hY3VsYXJOYW1lIjoicm9jb3RpbGxvIiwibGFuZ3VhZ2UiOiJzcGEifSx7InZlcm5hY3VsYXJOYW1lIjoic3F1YXNoIHBlcHBlciIsImxhbmd1YWdlIjoiZW5nIn0seyJ2ZXJuYWN1bGFyTmFtZSI6ImhhYmFuZXJvIHBlcHBlciIsImxhbmd1YWdlIjoiZW5nIn0seyJ2ZXJuYWN1bGFyTmFtZSI6ImJpcXVpbmhvIiwibGFuZ3VhZ2UiOiJwb3IifSx7InZlcm5hY3VsYXJOYW1lIjoiY3VtYXJpLWRvLXBhcsOhIiwibGFuZ3VhZ2UiOiJwb3IifSx7InZlcm5hY3VsYXJOYW1lIjoiaGFiYW5lcm8iLCJsYW5ndWFnZSI6InNwYSJ9LHsidmVybmFjdWxhck5hbWUiOiJtdXJ1cGkiLCJsYW5ndWFnZSI6InBvciJ9LHsidmVybmFjdWxhck5hbWUiOiJwaW1lbnRhIiwibGFuZ3VhZ2UiOiJwb3IifSx7InZlcm5hY3VsYXJOYW1lIjoicGltZW50YS1iaXF1aW5obyIsImxhbmd1YWdlIjoicG9yIn0seyJ2ZXJuYWN1bGFyTmFtZSI6InBpbWVudGEtY3VtYXJpLWRvLXBhcsOhIiwibGFuZ3VhZ2UiOiJwb3IifSx7InZlcm5hY3VsYXJOYW1lIjoicGltZW50YS1kZS1ib2RlIiwibGFuZ3VhZ2UiOiJwb3IifSx7InZlcm5hY3VsYXJOYW1lIjoicGltZW50YS1kZS1jaGVpcm8iLCJsYW5ndWFnZSI6InBvciJ9LHsidmVybmFjdWxhck5hbWUiOiJwaW1lbnRhLWhhYmFuZXJvIiwibGFuZ3VhZ2UiOiJwb3IifSx7InZlcm5hY3VsYXJOYW1lIjoiU2NvdGNoIGJvbm5ldCJ9LHsidmVybmFjdWxhck5hbWUiOiJib25uZXQgcGVwcGVyIn0seyJ2ZXJuYWN1bGFyTmFtZSI6ImhhYmFuZXJvIn0seyJ2ZXJuYWN1bGFyTmFtZSI6InBpcmkgcGlyaSJ9LHsidmVybmFjdWxhck5hbWUiOiJyb2NvdGlsbG8ifV0sInN5bm9ueW0iOmZhbHNlLCJoaWdoZXJDbGFzc2lmaWNhdGlvbk1hcCI6eyI2IjoiUGxhbnRhZSIsIjc3MDc3MjgiOiJUcmFjaGVvcGh5dGEiLCIyMjAiOiJNYWdub2xpb3BzaWRhIiwiMTE3NiI6IlNvbGFuYWxlcyIsIjc3MTciOiJTb2xhbmFjZWFlIiwiMjkzMjkzNyI6IkNhcHNpY3VtIn0sImNsYXNzIjoiTWFnbm9saW9wc2lkYSJ9XSwiZmFjZXRzIjpbXX0= + recorded_at: Sun, 14 Jan 2024 10:17:51 GMT +- request: + method: get + uri: https://api.gbif.org/v1/species/2932942 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday/v1.10.3 Gbif/v0.2.0 + X-USER-AGENT: + - Faraday/v1.10.3 Gbif/v0.2.0 + response: + status: + code: 200 + message: OK + headers: + vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + x-content-type-options: + - nosniff + x-xss-protection: + - 1; mode=block + pragma: + - no-cache + expires: + - '0' + x-frame-options: + - DENY + content-type: + - application/json + date: + - Sun, 14 Jan 2024 10:05:22 GMT + cache-control: + - public, max-age=3601 + x-varnish: + - 974291508 987791388 + age: + - '750' + via: + - 1.1 varnish (Varnish/6.0) + accept-ranges: + - bytes + content-length: + - '962' + connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"key":2932942,"nubKey":2932942,"nameKey":1970347,"taxonID":"gbif:2932942","kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Capsicum","species":"Capsicum + chinense","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2932937,"speciesKey":2932942,"datasetKey":"d7dddbf4-2cf0-4f39-9b2a-bb099caae36c","constituentKey":"7ddf754f-d193-4cc9-b351-99906754a03b","parentKey":2932937,"parent":"Capsicum","scientificName":"Capsicum + chinense Jacq.","canonicalName":"Capsicum chinense","vernacularName":"Habanero + pepper","authorship":"Jacq.","nameType":"SCIENTIFIC","rank":"SPECIES","origin":"SOURCE","taxonomicStatus":"ACCEPTED","nomenclaturalStatus":[],"remarks":"","publishedIn":"Jacq. + (1776). In: Hort. Bot. Vindobon. 3: 38, T. 67.","numDescendants":0,"lastCrawled":"2023-08-22T23:20:59.545+00:00","lastInterpreted":"2023-08-22T23:11:38.099+00:00","issues":[],"class":"Magnoliopsida"}' + recorded_at: Sun, 14 Jan 2024 10:17:52 GMT +- request: + method: get + uri: https://api.gbif.org/v1/occurrence/search?hasCoordinate=true&limit=3&mediatype=StillImage&offset=0&taxonKey=2932942 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday/v1.10.3 Gbif/v0.2.0 + X-USER-AGENT: + - Faraday/v1.10.3 Gbif/v0.2.0 + response: + status: + code: 200 + message: OK + headers: + vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + x-content-type-options: + - nosniff + x-xss-protection: + - 1; mode=block + pragma: + - no-cache + expires: + - '0' + x-frame-options: + - DENY + content-type: + - application/json + date: + - Sun, 14 Jan 2024 10:17:54 GMT + cache-control: + - public, max-age=600 + x-varnish: + - '988283449' + age: + - '0' + via: + - 1.1 varnish (Varnish/6.0) + accept-ranges: + - bytes + content-length: + - '12146' + connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: !binary |- + eyJvZmZzZXQiOjAsImxpbWl0IjozLCJlbmRPZlJlY29yZHMiOmZhbHNlLCJjb3VudCI6MjM3LCJyZXN1bHRzIjpbeyJrZXkiOjQ1MDA2NjYwODQsImRhdGFzZXRLZXkiOiI1MGM5NTA5ZC0yMmM3LTRhMjItYTQ3ZC04YzQ4NDI1ZWY0YTciLCJwdWJsaXNoaW5nT3JnS2V5IjoiMjhlYjFhM2YtMWMxNS00YTk1LTkzMWEtNGFmOTBlY2I1NzRkIiwiaW5zdGFsbGF0aW9uS2V5IjoiOTk3NDQ4YTgtZjc2Mi0xMWUxLWE0MzktMDAxNDVlYjQ1ZTlhIiwiaG9zdGluZ09yZ2FuaXphdGlvbktleSI6IjI4ZWIxYTNmLTFjMTUtNGE5NS05MzFhLTRhZjkwZWNiNTc0ZCIsInB1Ymxpc2hpbmdDb3VudHJ5IjoiVVMiLCJwcm90b2NvbCI6IkRXQ19BUkNISVZFIiwibGFzdENyYXdsZWQiOiIyMDI0LTAxLTA5VDAzOjMzOjM4LjczNSswMDowMCIsImxhc3RQYXJzZWQiOiIyMDI0LTAxLTA5VDE0OjE5OjA3LjI4NiswMDowMCIsImNyYXdsSWQiOjQyNiwiZXh0ZW5zaW9ucyI6eyJodHRwOi8vcnMuZ2JpZi5vcmcvdGVybXMvMS4wL011bHRpbWVkaWEiOlt7Imh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9jcmVhdGVkIjoiMjAyMy0wMS0yNlQxMzowOTo0NFoiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvbGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LzQuMC8iLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvdHlwZSI6IlN0aWxsSW1hZ2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvZm9ybWF0IjoiaW1hZ2UvanBlZyIsImh0dHA6Ly9ycy50ZHdnLm9yZy9kd2MvdGVybXMvY2F0YWxvZ051bWJlciI6IjI3NTMxNzg5MSIsImh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9yZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL3Bob3Rvcy8yNzUzMTc4OTEiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmlnaHRzSG9sZGVyIjoiTWFyayBSaWNobWFuIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2NyZWF0b3IiOiJNYXJrIFJpY2htYW4iLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzI3NTMxNzg5MS9vcmlnaW5hbC5qcGcifSx7Imh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9jcmVhdGVkIjoiMjAyMy0wMS0yNlQxMzowOTo0NFoiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvbGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LzQuMC8iLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvdHlwZSI6IlN0aWxsSW1hZ2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvZm9ybWF0IjoiaW1hZ2UvanBlZyIsImh0dHA6Ly9ycy50ZHdnLm9yZy9kd2MvdGVybXMvY2F0YWxvZ051bWJlciI6IjI3NTMxNzkyMSIsImh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9yZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL3Bob3Rvcy8yNzUzMTc5MjEiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmlnaHRzSG9sZGVyIjoiTWFyayBSaWNobWFuIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2NyZWF0b3IiOiJNYXJrIFJpY2htYW4iLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzI3NTMxNzkyMS9vcmlnaW5hbC5qcGcifV19LCJiYXNpc09mUmVjb3JkIjoiSFVNQU5fT0JTRVJWQVRJT04iLCJvY2N1cnJlbmNlU3RhdHVzIjoiUFJFU0VOVCIsInRheG9uS2V5IjoyOTMyOTQyLCJraW5nZG9tS2V5Ijo2LCJwaHlsdW1LZXkiOjc3MDc3MjgsImNsYXNzS2V5IjoyMjAsIm9yZGVyS2V5IjoxMTc2LCJmYW1pbHlLZXkiOjc3MTcsImdlbnVzS2V5IjoyOTMyOTM3LCJzcGVjaWVzS2V5IjoyOTMyOTQyLCJhY2NlcHRlZFRheG9uS2V5IjoyOTMyOTQyLCJzY2llbnRpZmljTmFtZSI6IkNhcHNpY3VtIGNoaW5lbnNlIEphY3EuIiwiYWNjZXB0ZWRTY2llbnRpZmljTmFtZSI6IkNhcHNpY3VtIGNoaW5lbnNlIEphY3EuIiwia2luZ2RvbSI6IlBsYW50YWUiLCJwaHlsdW0iOiJUcmFjaGVvcGh5dGEiLCJvcmRlciI6IlNvbGFuYWxlcyIsImZhbWlseSI6IlNvbGFuYWNlYWUiLCJnZW51cyI6IkNhcHNpY3VtIiwic3BlY2llcyI6IkNhcHNpY3VtIGNoaW5lbnNlIiwiZ2VuZXJpY05hbWUiOiJDYXBzaWN1bSIsInNwZWNpZmljRXBpdGhldCI6ImNoaW5lbnNlIiwidGF4b25SYW5rIjoiU1BFQ0lFUyIsInRheG9ub21pY1N0YXR1cyI6IkFDQ0VQVEVEIiwiaXVjblJlZExpc3RDYXRlZ29yeSI6Ik5FIiwiZGF0ZUlkZW50aWZpZWQiOiIyMDIzLTA1LTAzVDA0OjI1OjMxIiwiZGVjaW1hbExhdGl0dWRlIjo5LjE3MzkwOCwiZGVjaW1hbExvbmdpdHVkZSI6LTgzLjczNTkzOCwiY29vcmRpbmF0ZVVuY2VydGFpbnR5SW5NZXRlcnMiOjYuMCwiY29udGluZW50IjoiTk9SVEhfQU1FUklDQSIsInN0YXRlUHJvdmluY2UiOiJQdW50YXJlbmFzIiwiZ2FkbSI6eyJsZXZlbDAiOnsiZ2lkIjoiQ1JJIiwibmFtZSI6IkNvc3RhIFJpY2EifSwibGV2ZWwxIjp7ImdpZCI6IkNSSS42XzEiLCJuYW1lIjoiUHVudGFyZW5hcyJ9LCJsZXZlbDIiOnsiZ2lkIjoiQ1JJLjYuOV8xIiwibmFtZSI6Ik9zYSJ9LCJsZXZlbDMiOnsiZ2lkIjoiQ1JJLjYuOS4xXzEiLCJuYW1lIjoiQmFoaWEgQmFsbGVuYSJ9fSwieWVhciI6MjAyMywibW9udGgiOjEsImRheSI6MjYsImV2ZW50RGF0ZSI6IjIwMjMtMDEtMjZUMDc6MDk6NDQiLCJpc3N1ZXMiOlsiQ09PUkRJTkFURV9ST1VOREVEIiwiQ09OVElORU5UX0RFUklWRURfRlJPTV9DT09SRElOQVRFUyIsIlRBWE9OX01BVENIX1RBWE9OX0lEX0lHTk9SRUQiXSwibW9kaWZpZWQiOiIyMDIzLTEyLTE4VDE5OjA0OjM5LjAwMCswMDowMCIsImxhc3RJbnRlcnByZXRlZCI6IjIwMjQtMDEtMDlUMTQ6MTk6MDcuMjg2KzAwOjAwIiwicmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9vYnNlcnZhdGlvbnMvMTU5MzUzNTA3IiwibGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LzQuMC9sZWdhbGNvZGUiLCJpZGVudGlmaWVycyI6W3siaWRlbnRpZmllciI6IjE1OTM1MzUwNyJ9XSwibWVkaWEiOlt7InR5cGUiOiJTdGlsbEltYWdlIiwiZm9ybWF0IjoiaW1hZ2UvanBlZyIsInJlZmVyZW5jZXMiOiJodHRwczovL3d3dy5pbmF0dXJhbGlzdC5vcmcvcGhvdG9zLzI3NTMxNzg5MSIsImNyZWF0ZWQiOiIyMDIzLTAxLTI2VDEzOjA5OjQ0LjAwMCswMDowMCIsImNyZWF0b3IiOiJNYXJrIFJpY2htYW4iLCJwdWJsaXNoZXIiOiJpTmF0dXJhbGlzdCIsImxpY2Vuc2UiOiJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS80LjAvIiwicmlnaHRzSG9sZGVyIjoiTWFyayBSaWNobWFuIiwiaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzI3NTMxNzg5MS9vcmlnaW5hbC5qcGcifSx7InR5cGUiOiJTdGlsbEltYWdlIiwiZm9ybWF0IjoiaW1hZ2UvanBlZyIsInJlZmVyZW5jZXMiOiJodHRwczovL3d3dy5pbmF0dXJhbGlzdC5vcmcvcGhvdG9zLzI3NTMxNzkyMSIsImNyZWF0ZWQiOiIyMDIzLTAxLTI2VDEzOjA5OjQ0LjAwMCswMDowMCIsImNyZWF0b3IiOiJNYXJrIFJpY2htYW4iLCJwdWJsaXNoZXIiOiJpTmF0dXJhbGlzdCIsImxpY2Vuc2UiOiJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS80LjAvIiwicmlnaHRzSG9sZGVyIjoiTWFyayBSaWNobWFuIiwiaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzI3NTMxNzkyMS9vcmlnaW5hbC5qcGcifV0sImZhY3RzIjpbXSwicmVsYXRpb25zIjpbXSwiaXNJbkNsdXN0ZXIiOmZhbHNlLCJkYXRhc2V0TmFtZSI6ImlOYXR1cmFsaXN0IHJlc2VhcmNoLWdyYWRlIG9ic2VydmF0aW9ucyIsInJlY29yZGVkQnkiOiJNYXJrIFJpY2htYW4iLCJpZGVudGlmaWVkQnkiOiJNYXJrIFJpY2htYW4iLCJnZW9kZXRpY0RhdHVtIjoiV0dTODQiLCJjbGFzcyI6Ik1hZ25vbGlvcHNpZGEiLCJjb3VudHJ5Q29kZSI6IkNSIiwicmVjb3JkZWRCeUlEcyI6W10sImlkZW50aWZpZWRCeUlEcyI6W10sImNvdW50cnkiOiJDb3N0YSBSaWNhIiwicmlnaHRzSG9sZGVyIjoiTWFyayBSaWNobWFuIiwiaWRlbnRpZmllciI6IjE1OTM1MzUwNyIsImh0dHA6Ly91bmtub3duLm9yZy9uaWNrIjoiaW1hc29uZ3N0ZXIiLCJ2ZXJiYXRpbUV2ZW50RGF0ZSI6IjIwMjMtMDEtMjYgMDc6MDk6NDQtMDY6MDAiLCJnYmlmSUQiOiI0NTAwNjY2MDg0IiwidmVyYmF0aW1Mb2NhbGl0eSI6IkNhbGxlIFV2aXRhLCBPc2EsIFB1bnRhcmVuYXMsIENSIiwiY29sbGVjdGlvbkNvZGUiOiJPYnNlcnZhdGlvbnMiLCJvY2N1cnJlbmNlSUQiOiJodHRwczovL3d3dy5pbmF0dXJhbGlzdC5vcmcvb2JzZXJ2YXRpb25zLzE1OTM1MzUwNyIsInRheG9uSUQiOiI2OTEzNSIsImNhdGFsb2dOdW1iZXIiOiIxNTkzNTM1MDciLCJpbnN0aXR1dGlvbkNvZGUiOiJpTmF0dXJhbGlzdCIsImV2ZW50VGltZSI6IjA3OjA5OjQ0LTA2OjAwIiwiaHR0cDovL3Vua25vd24ub3JnL2NhcHRpdmUiOiJ3aWxkIiwiaWRlbnRpZmljYXRpb25JRCI6IjM1ODI2NzQzOSJ9LHsia2V5Ijo0MDgwOTc3NTQ0LCJkYXRhc2V0S2V5IjoiNTBjOTUwOWQtMjJjNy00YTIyLWE0N2QtOGM0ODQyNWVmNGE3IiwicHVibGlzaGluZ09yZ0tleSI6IjI4ZWIxYTNmLTFjMTUtNGE5NS05MzFhLTRhZjkwZWNiNTc0ZCIsImluc3RhbGxhdGlvbktleSI6Ijk5NzQ0OGE4LWY3NjItMTFlMS1hNDM5LTAwMTQ1ZWI0NWU5YSIsImhvc3RpbmdPcmdhbml6YXRpb25LZXkiOiIyOGViMWEzZi0xYzE1LTRhOTUtOTMxYS00YWY5MGVjYjU3NGQiLCJwdWJsaXNoaW5nQ291bnRyeSI6IlVTIiwicHJvdG9jb2wiOiJEV0NfQVJDSElWRSIsImxhc3RDcmF3bGVkIjoiMjAyNC0wMS0wOVQwMzozMzozOC43MzUrMDA6MDAiLCJsYXN0UGFyc2VkIjoiMjAyNC0wMS0wOVQxMzoxNTo0My4xNzgrMDA6MDAiLCJjcmF3bElkIjo0MjYsImV4dGVuc2lvbnMiOnsiaHR0cDovL3JzLmdiaWYub3JnL3Rlcm1zLzEuMC9NdWx0aW1lZGlhIjpbeyJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRlZCI6IjIwMjMtMDMtMTRUMTE6Mjc6MTdaIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2xpY2Vuc2UiOiJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS1uYy80LjAvIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL3R5cGUiOiJTdGlsbEltYWdlIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2Zvcm1hdCI6ImltYWdlL2pwZWciLCJodHRwOi8vcnMudGR3Zy5vcmcvZHdjL3Rlcm1zL2NhdGFsb2dOdW1iZXIiOiIyNjExNDQwMjciLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9waG90b3MvMjYxMTQ0MDI3IiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL3B1Ymxpc2hlciI6ImlOYXR1cmFsaXN0IiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL3JpZ2h0c0hvbGRlciI6InZhbGVudGluYV9wcmFuZGkiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRvciI6InZhbGVudGluYV9wcmFuZGkiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzI2MTE0NDAyNy9vcmlnaW5hbC5qcGcifV19LCJiYXNpc09mUmVjb3JkIjoiSFVNQU5fT0JTRVJWQVRJT04iLCJvY2N1cnJlbmNlU3RhdHVzIjoiUFJFU0VOVCIsInRheG9uS2V5IjoyOTMyOTQyLCJraW5nZG9tS2V5Ijo2LCJwaHlsdW1LZXkiOjc3MDc3MjgsImNsYXNzS2V5IjoyMjAsIm9yZGVyS2V5IjoxMTc2LCJmYW1pbHlLZXkiOjc3MTcsImdlbnVzS2V5IjoyOTMyOTM3LCJzcGVjaWVzS2V5IjoyOTMyOTQyLCJhY2NlcHRlZFRheG9uS2V5IjoyOTMyOTQyLCJzY2llbnRpZmljTmFtZSI6IkNhcHNpY3VtIGNoaW5lbnNlIEphY3EuIiwiYWNjZXB0ZWRTY2llbnRpZmljTmFtZSI6IkNhcHNpY3VtIGNoaW5lbnNlIEphY3EuIiwia2luZ2RvbSI6IlBsYW50YWUiLCJwaHlsdW0iOiJUcmFjaGVvcGh5dGEiLCJvcmRlciI6IlNvbGFuYWxlcyIsImZhbWlseSI6IlNvbGFuYWNlYWUiLCJnZW51cyI6IkNhcHNpY3VtIiwic3BlY2llcyI6IkNhcHNpY3VtIGNoaW5lbnNlIiwiZ2VuZXJpY05hbWUiOiJDYXBzaWN1bSIsInNwZWNpZmljRXBpdGhldCI6ImNoaW5lbnNlIiwidGF4b25SYW5rIjoiU1BFQ0lFUyIsInRheG9ub21pY1N0YXR1cyI6IkFDQ0VQVEVEIiwiaXVjblJlZExpc3RDYXRlZ29yeSI6Ik5FIiwiZGF0ZUlkZW50aWZpZWQiOiIyMDIzLTAzLTE3VDAyOjIyOjQ5IiwiZGVjaW1hbExhdGl0dWRlIjotMjQuMTY4NzU4LCJkZWNpbWFsTG9uZ2l0dWRlIjotNDYuNzg1NzA4LCJjb29yZGluYXRlVW5jZXJ0YWludHlJbk1ldGVycyI6OS4wLCJjb250aW5lbnQiOiJTT1VUSF9BTUVSSUNBIiwic3RhdGVQcm92aW5jZSI6IlPDo28gUGF1bG8iLCJnYWRtIjp7ImxldmVsMCI6eyJnaWQiOiJCUkEiLCJuYW1lIjoiQnJhemlsIn0sImxldmVsMSI6eyJnaWQiOiJCUkEuMjVfMSIsIm5hbWUiOiJTw6NvIFBhdWxvIn0sImxldmVsMiI6eyJnaWQiOiJCUkEuMjUuMjU1XzIiLCJuYW1lIjoiSXRhbmhhw6ltIn19LCJ5ZWFyIjoyMDIzLCJtb250aCI6MywiZGF5IjoxNCwiZXZlbnREYXRlIjoiMjAyMy0wMy0xNFQwODoyNzoxNyIsImlzc3VlcyI6WyJDT09SRElOQVRFX1JPVU5ERUQiLCJDT05USU5FTlRfREVSSVZFRF9GUk9NX0NPT1JESU5BVEVTIiwiVEFYT05fTUFUQ0hfVEFYT05fSURfSUdOT1JFRCJdLCJtb2RpZmllZCI6IjIwMjMtMDQtMDlUMDI6MTI6MjguMDAwKzAwOjAwIiwibGFzdEludGVycHJldGVkIjoiMjAyNC0wMS0wOVQxMzoxNTo0My4xNzgrMDA6MDAiLCJyZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL29ic2VydmF0aW9ucy8xNTEzNTc1MTAiLCJsaWNlbnNlIjoiaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMvNC4wL2xlZ2FsY29kZSIsImlkZW50aWZpZXJzIjpbeyJpZGVudGlmaWVyIjoiMTUxMzU3NTEwIn1dLCJtZWRpYSI6W3sidHlwZSI6IlN0aWxsSW1hZ2UiLCJmb3JtYXQiOiJpbWFnZS9qcGVnIiwicmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9waG90b3MvMjYxMTQ0MDI3IiwiY3JlYXRlZCI6IjIwMjMtMDMtMTRUMTE6Mjc6MTcuMDAwKzAwOjAwIiwiY3JlYXRvciI6InZhbGVudGluYV9wcmFuZGkiLCJwdWJsaXNoZXIiOiJpTmF0dXJhbGlzdCIsImxpY2Vuc2UiOiJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS1uYy80LjAvIiwicmlnaHRzSG9sZGVyIjoidmFsZW50aW5hX3ByYW5kaSIsImlkZW50aWZpZXIiOiJodHRwczovL2luYXR1cmFsaXN0LW9wZW4tZGF0YS5zMy5hbWF6b25hd3MuY29tL3Bob3Rvcy8yNjExNDQwMjcvb3JpZ2luYWwuanBnIn1dLCJmYWN0cyI6W10sInJlbGF0aW9ucyI6W10sImlzSW5DbHVzdGVyIjpmYWxzZSwiZGF0YXNldE5hbWUiOiJpTmF0dXJhbGlzdCByZXNlYXJjaC1ncmFkZSBvYnNlcnZhdGlvbnMiLCJyZWNvcmRlZEJ5IjoidmFsZW50aW5hX3ByYW5kaSIsImlkZW50aWZpZWRCeSI6InZhbGVudGluYV9wcmFuZGkiLCJnZW9kZXRpY0RhdHVtIjoiV0dTODQiLCJjbGFzcyI6Ik1hZ25vbGlvcHNpZGEiLCJjb3VudHJ5Q29kZSI6IkJSIiwicmVjb3JkZWRCeUlEcyI6W10sImlkZW50aWZpZWRCeUlEcyI6W10sImNvdW50cnkiOiJCcmF6aWwiLCJyaWdodHNIb2xkZXIiOiJ2YWxlbnRpbmFfcHJhbmRpIiwiaWRlbnRpZmllciI6IjE1MTM1NzUxMCIsImh0dHA6Ly91bmtub3duLm9yZy9uaWNrIjoidmFsZW50aW5hX3ByYW5kaSIsInZlcmJhdGltRXZlbnREYXRlIjoiMjAyMy0wMy0xNCAwODoyNzoxNy0wMzowMCIsImdiaWZJRCI6IjQwODA5Nzc1NDQiLCJ2ZXJiYXRpbUxvY2FsaXR5IjoiRVRFQyBkZSBJdGFuaGHDqW0sIEl0YW5oYcOpbSwgU1AsIEJSIiwiY29sbGVjdGlvbkNvZGUiOiJPYnNlcnZhdGlvbnMiLCJvY2N1cnJlbmNlSUQiOiJodHRwczovL3d3dy5pbmF0dXJhbGlzdC5vcmcvb2JzZXJ2YXRpb25zLzE1MTM1NzUxMCIsInRheG9uSUQiOiI2OTEzNSIsImNhdGFsb2dOdW1iZXIiOiIxNTEzNTc1MTAiLCJpbnN0aXR1dGlvbkNvZGUiOiJpTmF0dXJhbGlzdCIsImV2ZW50VGltZSI6IjA4OjI3OjE3LTAzOjAwIiwiaHR0cDovL3Vua25vd24ub3JnL2NhcHRpdmUiOiJ3aWxkIiwiaWRlbnRpZmljYXRpb25JRCI6IjM0MTg5NjQ4MSJ9LHsia2V5Ijo0MDgwOTczNTM0LCJkYXRhc2V0S2V5IjoiNTBjOTUwOWQtMjJjNy00YTIyLWE0N2QtOGM0ODQyNWVmNGE3IiwicHVibGlzaGluZ09yZ0tleSI6IjI4ZWIxYTNmLTFjMTUtNGE5NS05MzFhLTRhZjkwZWNiNTc0ZCIsImluc3RhbGxhdGlvbktleSI6Ijk5NzQ0OGE4LWY3NjItMTFlMS1hNDM5LTAwMTQ1ZWI0NWU5YSIsImhvc3RpbmdPcmdhbml6YXRpb25LZXkiOiIyOGViMWEzZi0xYzE1LTRhOTUtOTMxYS00YWY5MGVjYjU3NGQiLCJwdWJsaXNoaW5nQ291bnRyeSI6IlVTIiwicHJvdG9jb2wiOiJEV0NfQVJDSElWRSIsImxhc3RDcmF3bGVkIjoiMjAyNC0wMS0wOVQwMzozMzozOC43MzUrMDA6MDAiLCJsYXN0UGFyc2VkIjoiMjAyNC0wMS0wOVQxMzoxNzo1Ni44NDYrMDA6MDAiLCJjcmF3bElkIjo0MjYsImV4dGVuc2lvbnMiOnsiaHR0cDovL3JzLmdiaWYub3JnL3Rlcm1zLzEuMC9NdWx0aW1lZGlhIjpbeyJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRlZCI6IjIwMjMtMDMtMjhUMDg6NDg6MTctMDc6MDAiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvbGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLzQuMC8iLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvdHlwZSI6IlN0aWxsSW1hZ2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvZm9ybWF0IjoiaW1hZ2UvanBlZyIsImh0dHA6Ly9ycy50ZHdnLm9yZy9kd2MvdGVybXMvY2F0YWxvZ051bWJlciI6IjI2MzQ3NjQ5MyIsImh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9yZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL3Bob3Rvcy8yNjM0NzY0OTMiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmlnaHRzSG9sZGVyIjoiYmlhY2F0YWxhbmkiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRvciI6ImJpYWNhdGFsYW5pIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2lkZW50aWZpZXIiOiJodHRwczovL2luYXR1cmFsaXN0LW9wZW4tZGF0YS5zMy5hbWF6b25hd3MuY29tL3Bob3Rvcy8yNjM0NzY0OTMvb3JpZ2luYWwuanBlZyJ9XX0sImJhc2lzT2ZSZWNvcmQiOiJIVU1BTl9PQlNFUlZBVElPTiIsIm9jY3VycmVuY2VTdGF0dXMiOiJQUkVTRU5UIiwidGF4b25LZXkiOjI5MzI5NDIsImtpbmdkb21LZXkiOjYsInBoeWx1bUtleSI6NzcwNzcyOCwiY2xhc3NLZXkiOjIyMCwib3JkZXJLZXkiOjExNzYsImZhbWlseUtleSI6NzcxNywiZ2VudXNLZXkiOjI5MzI5MzcsInNwZWNpZXNLZXkiOjI5MzI5NDIsImFjY2VwdGVkVGF4b25LZXkiOjI5MzI5NDIsInNjaWVudGlmaWNOYW1lIjoiQ2Fwc2ljdW0gY2hpbmVuc2UgSmFjcS4iLCJhY2NlcHRlZFNjaWVudGlmaWNOYW1lIjoiQ2Fwc2ljdW0gY2hpbmVuc2UgSmFjcS4iLCJraW5nZG9tIjoiUGxhbnRhZSIsInBoeWx1bSI6IlRyYWNoZW9waHl0YSIsIm9yZGVyIjoiU29sYW5hbGVzIiwiZmFtaWx5IjoiU29sYW5hY2VhZSIsImdlbnVzIjoiQ2Fwc2ljdW0iLCJzcGVjaWVzIjoiQ2Fwc2ljdW0gY2hpbmVuc2UiLCJnZW5lcmljTmFtZSI6IkNhcHNpY3VtIiwic3BlY2lmaWNFcGl0aGV0IjoiY2hpbmVuc2UiLCJ0YXhvblJhbmsiOiJTUEVDSUVTIiwidGF4b25vbWljU3RhdHVzIjoiQUNDRVBURUQiLCJpdWNuUmVkTGlzdENhdGVnb3J5IjoiTkUiLCJkYXRlSWRlbnRpZmllZCI6IjIwMjMtMDMtMjhUMTI6NDA6MjkiLCJkZWNpbWFsTGF0aXR1ZGUiOi0yNC4xNjg4OTUsImRlY2ltYWxMb25naXR1ZGUiOi00Ni43ODUzNTUsImNvb3JkaW5hdGVVbmNlcnRhaW50eUluTWV0ZXJzIjoyMDguMCwiY29udGluZW50IjoiU09VVEhfQU1FUklDQSIsInN0YXRlUHJvdmluY2UiOiJTw6NvIFBhdWxvIiwiZ2FkbSI6eyJsZXZlbDAiOnsiZ2lkIjoiQlJBIiwibmFtZSI6IkJyYXppbCJ9LCJsZXZlbDEiOnsiZ2lkIjoiQlJBLjI1XzEiLCJuYW1lIjoiU8OjbyBQYXVsbyJ9LCJsZXZlbDIiOnsiZ2lkIjoiQlJBLjI1LjI1NV8yIiwibmFtZSI6Ikl0YW5oYcOpbSJ9fSwieWVhciI6MjAyMywibW9udGgiOjMsImRheSI6MjgsImV2ZW50RGF0ZSI6IjIwMjMtMDMtMjhUMDg6NDg6MTciLCJpc3N1ZXMiOlsiQ09PUkRJTkFURV9ST1VOREVEIiwiQ09OVElORU5UX0RFUklWRURfRlJPTV9DT09SRElOQVRFUyIsIlRBWE9OX01BVENIX1RBWE9OX0lEX0lHTk9SRUQiXSwibW9kaWZpZWQiOiIyMDIzLTA0LTA5VDAxOjQ4OjM4LjAwMCswMDowMCIsImxhc3RJbnRlcnByZXRlZCI6IjIwMjQtMDEtMDlUMTM6MTc6NTYuODQ2KzAwOjAwIiwicmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9vYnNlcnZhdGlvbnMvMTUyNjEyODc4IiwibGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLzQuMC9sZWdhbGNvZGUiLCJpZGVudGlmaWVycyI6W3siaWRlbnRpZmllciI6IjE1MjYxMjg3OCJ9XSwibWVkaWEiOlt7InR5cGUiOiJTdGlsbEltYWdlIiwiZm9ybWF0IjoiaW1hZ2UvanBlZyIsInJlZmVyZW5jZXMiOiJodHRwczovL3d3dy5pbmF0dXJhbGlzdC5vcmcvcGhvdG9zLzI2MzQ3NjQ5MyIsImNyZWF0ZWQiOiIyMDIzLTAzLTI4VDE1OjQ4OjE3LjAwMCswMDowMCIsImNyZWF0b3IiOiJiaWFjYXRhbGFuaSIsInB1Ymxpc2hlciI6ImlOYXR1cmFsaXN0IiwibGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLzQuMC8iLCJyaWdodHNIb2xkZXIiOiJiaWFjYXRhbGFuaSIsImlkZW50aWZpZXIiOiJodHRwczovL2luYXR1cmFsaXN0LW9wZW4tZGF0YS5zMy5hbWF6b25hd3MuY29tL3Bob3Rvcy8yNjM0NzY0OTMvb3JpZ2luYWwuanBlZyJ9XSwiZmFjdHMiOltdLCJyZWxhdGlvbnMiOltdLCJpc0luQ2x1c3RlciI6ZmFsc2UsImRhdGFzZXROYW1lIjoiaU5hdHVyYWxpc3QgcmVzZWFyY2gtZ3JhZGUgb2JzZXJ2YXRpb25zIiwicmVjb3JkZWRCeSI6ImJpYWNhdGFsYW5pIiwiaWRlbnRpZmllZEJ5IjoiYmlhY2F0YWxhbmkiLCJnZW9kZXRpY0RhdHVtIjoiV0dTODQiLCJjbGFzcyI6Ik1hZ25vbGlvcHNpZGEiLCJjb3VudHJ5Q29kZSI6IkJSIiwicmVjb3JkZWRCeUlEcyI6W10sImlkZW50aWZpZWRCeUlEcyI6W10sImNvdW50cnkiOiJCcmF6aWwiLCJyaWdodHNIb2xkZXIiOiJiaWFjYXRhbGFuaSIsImlkZW50aWZpZXIiOiIxNTI2MTI4NzgiLCJodHRwOi8vdW5rbm93bi5vcmcvbmljayI6ImJpYWNhdGFsYW5pIiwidmVyYmF0aW1FdmVudERhdGUiOiIyMDIzLTAzLTI4IDA4OjQ4OjE3IiwiZ2JpZklEIjoiNDA4MDk3MzUzNCIsInZlcmJhdGltTG9jYWxpdHkiOiJFVEVDIGRlIEl0YW5oYcOpbSIsImNvbGxlY3Rpb25Db2RlIjoiT2JzZXJ2YXRpb25zIiwib2NjdXJyZW5jZUlEIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL29ic2VydmF0aW9ucy8xNTI2MTI4NzgiLCJ0YXhvbklEIjoiNjkxMzUiLCJjYXRhbG9nTnVtYmVyIjoiMTUyNjEyODc4IiwiaW5zdGl0dXRpb25Db2RlIjoiaU5hdHVyYWxpc3QiLCJldmVudFRpbWUiOiIwODo0ODoxNy0wMzowMCIsInJlcHJvZHVjdGl2ZUNvbmRpdGlvbiI6ImZydWl0aW5nIiwiaHR0cDovL3Vua25vd24ub3JnL2NhcHRpdmUiOiJ3aWxkIiwiaWRlbnRpZmljYXRpb25JRCI6IjM0NDkyMjMzNCJ9XSwiZmFjZXRzIjpbXX0= + recorded_at: Sun, 14 Jan 2024 10:17:54 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/cassettes/GbifService/_update_crop/resolves_scientific_names.yml b/spec/cassettes/GbifService/_update_crop/resolves_scientific_names.yml new file mode 100644 index 000000000..1a6af37bb --- /dev/null +++ b/spec/cassettes/GbifService/_update_crop/resolves_scientific_names.yml @@ -0,0 +1,160 @@ +--- +http_interactions: +- request: + method: get + uri: https://api.gbif.org/v1/species/match?name=Solanum+lycopersicum + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday/v1.10.3 Gbif/v0.2.0 + X-USER-AGENT: + - Faraday/v1.10.3 Gbif/v0.2.0 + response: + status: + code: 200 + message: OK + headers: + vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + x-content-type-options: + - nosniff + x-xss-protection: + - 1; mode=block + pragma: + - no-cache + expires: + - '0' + x-frame-options: + - DENY + content-type: + - application/json + date: + - Sun, 14 Jan 2024 10:03:58 GMT + cache-control: + - public, max-age=3601 + x-varnish: + - 987201832 959415212 + age: + - '827' + via: + - 1.1 varnish (Varnish/6.0) + accept-ranges: + - bytes + content-length: + - '475' + connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"usageKey":2930137,"scientificName":"Solanum lycopersicum L.","canonicalName":"Solanum + lycopersicum","rank":"SPECIES","status":"ACCEPTED","confidence":98,"matchType":"EXACT","kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"synonym":false,"class":"Magnoliopsida"}' + recorded_at: Sun, 14 Jan 2024 10:17:45 GMT +- request: + method: get + uri: https://api.gbif.org/v1/species/2930137 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday/v1.10.3 Gbif/v0.2.0 + X-USER-AGENT: + - Faraday/v1.10.3 Gbif/v0.2.0 + response: + status: + code: 200 + message: OK + headers: + vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + x-content-type-options: + - nosniff + x-xss-protection: + - 1; mode=block + pragma: + - no-cache + expires: + - '0' + x-frame-options: + - DENY + content-type: + - application/json + date: + - Sun, 14 Jan 2024 10:03:58 GMT + cache-control: + - public, max-age=3601 + x-varnish: + - 940873195 979042621 + age: + - '827' + via: + - 1.1 varnish (Varnish/6.0) + accept-ranges: + - bytes + content-length: + - '938' + connection: + - keep-alive + body: + encoding: UTF-8 + string: '{"key":2930137,"nubKey":2930137,"nameKey":10463714,"taxonID":"gbif:2930137","kingdom":"Plantae","phylum":"Tracheophyta","order":"Solanales","family":"Solanaceae","genus":"Solanum","species":"Solanum + lycopersicum","kingdomKey":6,"phylumKey":7707728,"classKey":220,"orderKey":1176,"familyKey":7717,"genusKey":2928997,"speciesKey":2930137,"datasetKey":"d7dddbf4-2cf0-4f39-9b2a-bb099caae36c","constituentKey":"7ddf754f-d193-4cc9-b351-99906754a03b","parentKey":2928997,"parent":"Solanum","scientificName":"Solanum + lycopersicum L.","canonicalName":"Solanum lycopersicum","vernacularName":"Garden + tomato","authorship":"L.","nameType":"SCIENTIFIC","rank":"SPECIES","origin":"SOURCE","taxonomicStatus":"ACCEPTED","nomenclaturalStatus":[],"remarks":"","publishedIn":"L. + (1753). In: Sp. Pl. 185.","numDescendants":23,"lastCrawled":"2023-08-22T23:20:59.545+00:00","lastInterpreted":"2023-08-22T23:12:05.487+00:00","issues":[],"class":"Magnoliopsida"}' + recorded_at: Sun, 14 Jan 2024 10:17:47 GMT +- request: + method: get + uri: https://api.gbif.org/v1/occurrence/search?hasCoordinate=true&limit=3&mediatype=StillImage&offset=0&taxonKey=2930137 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Faraday/v1.10.3 Gbif/v0.2.0 + X-USER-AGENT: + - Faraday/v1.10.3 Gbif/v0.2.0 + response: + status: + code: 200 + message: OK + headers: + vary: + - Origin, Access-Control-Request-Method, Access-Control-Request-Headers + x-content-type-options: + - nosniff + x-xss-protection: + - 1; mode=block + pragma: + - no-cache + expires: + - '0' + x-frame-options: + - DENY + content-type: + - application/json + date: + - Sun, 14 Jan 2024 10:15:24 GMT + cache-control: + - public, max-age=600 + x-varnish: + - 988807796 990052778 + age: + - '143' + via: + - 1.1 varnish (Varnish/6.0) + accept-ranges: + - bytes + content-length: + - '12391' + connection: + - keep-alive + body: + encoding: ASCII-8BIT + string: !binary |- + eyJvZmZzZXQiOjAsImxpbWl0IjozLCJlbmRPZlJlY29yZHMiOmZhbHNlLCJjb3VudCI6NzY0NCwicmVzdWx0cyI6W3sia2V5Ijo0NTA3Njg4MTMwLCJkYXRhc2V0S2V5IjoiNTBjOTUwOWQtMjJjNy00YTIyLWE0N2QtOGM0ODQyNWVmNGE3IiwicHVibGlzaGluZ09yZ0tleSI6IjI4ZWIxYTNmLTFjMTUtNGE5NS05MzFhLTRhZjkwZWNiNTc0ZCIsImluc3RhbGxhdGlvbktleSI6Ijk5NzQ0OGE4LWY3NjItMTFlMS1hNDM5LTAwMTQ1ZWI0NWU5YSIsImhvc3RpbmdPcmdhbml6YXRpb25LZXkiOiIyOGViMWEzZi0xYzE1LTRhOTUtOTMxYS00YWY5MGVjYjU3NGQiLCJwdWJsaXNoaW5nQ291bnRyeSI6IlVTIiwicHJvdG9jb2wiOiJEV0NfQVJDSElWRSIsImxhc3RDcmF3bGVkIjoiMjAyNC0wMS0wOVQwMzozMzozOC43MzUrMDA6MDAiLCJsYXN0UGFyc2VkIjoiMjAyNC0wMS0wOVQxMzoyNjowMy44OTErMDA6MDAiLCJjcmF3bElkIjo0MjYsImV4dGVuc2lvbnMiOnsiaHR0cDovL3JzLmdiaWYub3JnL3Rlcm1zLzEuMC9NdWx0aW1lZGlhIjpbeyJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRlZCI6IjIwMjQtMDEtMDFUMTE6MDc6MDBaIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2xpY2Vuc2UiOiJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS1uYy80LjAvIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL3R5cGUiOiJTdGlsbEltYWdlIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2Zvcm1hdCI6ImltYWdlL2pwZWciLCJodHRwOi8vcnMudGR3Zy5vcmcvZHdjL3Rlcm1zL2NhdGFsb2dOdW1iZXIiOiIzNDM4NzQzNTAiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9waG90b3MvMzQzODc0MzUwIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL3B1Ymxpc2hlciI6ImlOYXR1cmFsaXN0IiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL3JpZ2h0c0hvbGRlciI6IkluZ2Vib3JnIHZhbiBMZWV1d2VuIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2NyZWF0b3IiOiJJbmdlYm9yZyB2YW4gTGVldXdlbiIsImh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9pZGVudGlmaWVyIjoiaHR0cHM6Ly9pbmF0dXJhbGlzdC1vcGVuLWRhdGEuczMuYW1hem9uYXdzLmNvbS9waG90b3MvMzQzODc0MzUwL29yaWdpbmFsLmpwZWcifV19LCJiYXNpc09mUmVjb3JkIjoiSFVNQU5fT0JTRVJWQVRJT04iLCJvY2N1cnJlbmNlU3RhdHVzIjoiUFJFU0VOVCIsInRheG9uS2V5IjoyOTMwMTM3LCJraW5nZG9tS2V5Ijo2LCJwaHlsdW1LZXkiOjc3MDc3MjgsImNsYXNzS2V5IjoyMjAsIm9yZGVyS2V5IjoxMTc2LCJmYW1pbHlLZXkiOjc3MTcsImdlbnVzS2V5IjoyOTI4OTk3LCJzcGVjaWVzS2V5IjoyOTMwMTM3LCJhY2NlcHRlZFRheG9uS2V5IjoyOTMwMTM3LCJzY2llbnRpZmljTmFtZSI6IlNvbGFudW0gbHljb3BlcnNpY3VtIEwuIiwiYWNjZXB0ZWRTY2llbnRpZmljTmFtZSI6IlNvbGFudW0gbHljb3BlcnNpY3VtIEwuIiwia2luZ2RvbSI6IlBsYW50YWUiLCJwaHlsdW0iOiJUcmFjaGVvcGh5dGEiLCJvcmRlciI6IlNvbGFuYWxlcyIsImZhbWlseSI6IlNvbGFuYWNlYWUiLCJnZW51cyI6IlNvbGFudW0iLCJzcGVjaWVzIjoiU29sYW51bSBseWNvcGVyc2ljdW0iLCJnZW5lcmljTmFtZSI6IlNvbGFudW0iLCJzcGVjaWZpY0VwaXRoZXQiOiJseWNvcGVyc2ljdW0iLCJ0YXhvblJhbmsiOiJTUEVDSUVTIiwidGF4b25vbWljU3RhdHVzIjoiQUNDRVBURUQiLCJpdWNuUmVkTGlzdENhdGVnb3J5IjoiTkUiLCJkYXRlSWRlbnRpZmllZCI6IjIwMjQtMDEtMDFUMTM6MDA6MjciLCJkZWNpbWFsTGF0aXR1ZGUiOjI4LjM5MDk2NCwiZGVjaW1hbExvbmdpdHVkZSI6LTE2LjYyNjY5NywiY29vcmRpbmF0ZVVuY2VydGFpbnR5SW5NZXRlcnMiOjQuMCwiY29udGluZW50IjoiQUZSSUNBIiwic3RhdGVQcm92aW5jZSI6IklzbGFzIENhbmFyaWFzIiwiZ2FkbSI6eyJsZXZlbDAiOnsiZ2lkIjoiRVNQIiwibmFtZSI6IlNwYWluIn0sImxldmVsMSI6eyJnaWQiOiJFU1AuMTRfMSIsIm5hbWUiOiJJc2xhcyBDYW5hcmlhcyJ9LCJsZXZlbDIiOnsiZ2lkIjoiRVNQLjE0LjJfMSIsIm5hbWUiOiJTYW50YSBDcnV6IGRlIFRlbmVyaWZlIn0sImxldmVsMyI6eyJnaWQiOiJFU1AuMTQuMi4xXzEiLCJuYW1lIjoibi5hLiAoMTQ0KSJ9fSwieWVhciI6MjAyNCwibW9udGgiOjEsImRheSI6MSwiZXZlbnREYXRlIjoiMjAyNC0wMS0wMVQxMTowNzowMCIsImlzc3VlcyI6WyJDT09SRElOQVRFX1JPVU5ERUQiLCJDT05USU5FTlRfREVSSVZFRF9GUk9NX0NPT1JESU5BVEVTIiwiVEFYT05fTUFUQ0hfVEFYT05fSURfSUdOT1JFRCJdLCJtb2RpZmllZCI6IjIwMjQtMDEtMDFUMTY6MzU6MDQuMDAwKzAwOjAwIiwibGFzdEludGVycHJldGVkIjoiMjAyNC0wMS0wOVQxMzoyNjowMy44OTErMDA6MDAiLCJyZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL29ic2VydmF0aW9ucy8xOTU0NjIyNjEiLCJsaWNlbnNlIjoiaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMvNC4wL2xlZ2FsY29kZSIsImlkZW50aWZpZXJzIjpbeyJpZGVudGlmaWVyIjoiMTk1NDYyMjYxIn1dLCJtZWRpYSI6W3sidHlwZSI6IlN0aWxsSW1hZ2UiLCJmb3JtYXQiOiJpbWFnZS9qcGVnIiwicmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9waG90b3MvMzQzODc0MzUwIiwiY3JlYXRlZCI6IjIwMjQtMDEtMDFUMTE6MDc6MDAuMDAwKzAwOjAwIiwiY3JlYXRvciI6IkluZ2Vib3JnIHZhbiBMZWV1d2VuIiwicHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJsaWNlbnNlIjoiaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMvNC4wLyIsInJpZ2h0c0hvbGRlciI6IkluZ2Vib3JnIHZhbiBMZWV1d2VuIiwiaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzM0Mzg3NDM1MC9vcmlnaW5hbC5qcGVnIn1dLCJmYWN0cyI6W10sInJlbGF0aW9ucyI6W10sImlzSW5DbHVzdGVyIjpmYWxzZSwiZGF0YXNldE5hbWUiOiJpTmF0dXJhbGlzdCByZXNlYXJjaC1ncmFkZSBvYnNlcnZhdGlvbnMiLCJyZWNvcmRlZEJ5IjoiSW5nZWJvcmcgdmFuIExlZXV3ZW4iLCJpZGVudGlmaWVkQnkiOiJJbmdlYm9yZyB2YW4gTGVldXdlbiIsImdlb2RldGljRGF0dW0iOiJXR1M4NCIsImNsYXNzIjoiTWFnbm9saW9wc2lkYSIsImNvdW50cnlDb2RlIjoiRVMiLCJyZWNvcmRlZEJ5SURzIjpbXSwiaWRlbnRpZmllZEJ5SURzIjpbXSwiY291bnRyeSI6IlNwYWluIiwicmlnaHRzSG9sZGVyIjoiSW5nZWJvcmcgdmFuIExlZXV3ZW4iLCJpZGVudGlmaWVyIjoiMTk1NDYyMjYxIiwiaHR0cDovL3Vua25vd24ub3JnL25pY2siOiJ3aWxkY2hyb21hIiwidmVyYmF0aW1FdmVudERhdGUiOiIyMDI0LTAxLTAxIDExOjA3OjAwIiwiZ2JpZklEIjoiNDUwNzY4ODEzMCIsInZlcmJhdGltTG9jYWxpdHkiOiJCYXJyYW5jbyBkZSBSdWl6LCAzODQyMCwgU2FudGEgQ3J1eiBkZSBUZW5lcmlmZSwgU3BhaW4iLCJjb2xsZWN0aW9uQ29kZSI6Ik9ic2VydmF0aW9ucyIsIm9jY3VycmVuY2VJRCI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9vYnNlcnZhdGlvbnMvMTk1NDYyMjYxIiwidGF4b25JRCI6IjUxNzM3IiwiY2F0YWxvZ051bWJlciI6IjE5NTQ2MjI2MSIsImluc3RpdHV0aW9uQ29kZSI6ImlOYXR1cmFsaXN0IiwiZXZlbnRUaW1lIjoiMTE6MDc6MDArMDA6MDAiLCJodHRwOi8vdW5rbm93bi5vcmcvY2FwdGl2ZSI6IndpbGQiLCJpZGVudGlmaWNhdGlvbklEIjoiNDM5NTkxNDI0In0seyJrZXkiOjQ1MDc5NTMzODcsImRhdGFzZXRLZXkiOiI1MGM5NTA5ZC0yMmM3LTRhMjItYTQ3ZC04YzQ4NDI1ZWY0YTciLCJwdWJsaXNoaW5nT3JnS2V5IjoiMjhlYjFhM2YtMWMxNS00YTk1LTkzMWEtNGFmOTBlY2I1NzRkIiwiaW5zdGFsbGF0aW9uS2V5IjoiOTk3NDQ4YTgtZjc2Mi0xMWUxLWE0MzktMDAxNDVlYjQ1ZTlhIiwiaG9zdGluZ09yZ2FuaXphdGlvbktleSI6IjI4ZWIxYTNmLTFjMTUtNGE5NS05MzFhLTRhZjkwZWNiNTc0ZCIsInB1Ymxpc2hpbmdDb3VudHJ5IjoiVVMiLCJwcm90b2NvbCI6IkRXQ19BUkNISVZFIiwibGFzdENyYXdsZWQiOiIyMDI0LTAxLTA5VDAzOjMzOjM4LjczNSswMDowMCIsImxhc3RQYXJzZWQiOiIyMDI0LTAxLTA5VDEzOjQ1OjI0Ljk2MSswMDowMCIsImNyYXdsSWQiOjQyNiwiZXh0ZW5zaW9ucyI6eyJodHRwOi8vcnMuZ2JpZi5vcmcvdGVybXMvMS4wL011bHRpbWVkaWEiOlt7Imh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9jcmVhdGVkIjoiMjAyNC0wMS0wMlQwNzozMTowNFoiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvbGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLzQuMC8iLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvdHlwZSI6IlN0aWxsSW1hZ2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvZm9ybWF0IjoiaW1hZ2UvanBlZyIsImh0dHA6Ly9ycy50ZHdnLm9yZy9kd2MvdGVybXMvY2F0YWxvZ051bWJlciI6IjM0NDA1ODExMyIsImh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9yZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL3Bob3Rvcy8zNDQwNTgxMTMiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmlnaHRzSG9sZGVyIjoid2Fud2lzYSBcdUQ4M0RcdURDOUUiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRvciI6Indhbndpc2EgXHVEODNEXHVEQzlFIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2lkZW50aWZpZXIiOiJodHRwczovL2luYXR1cmFsaXN0LW9wZW4tZGF0YS5zMy5hbWF6b25hd3MuY29tL3Bob3Rvcy8zNDQwNTgxMTMvb3JpZ2luYWwuanBlZyJ9XX0sImJhc2lzT2ZSZWNvcmQiOiJIVU1BTl9PQlNFUlZBVElPTiIsIm9jY3VycmVuY2VTdGF0dXMiOiJQUkVTRU5UIiwidGF4b25LZXkiOjI5MzAxMzcsImtpbmdkb21LZXkiOjYsInBoeWx1bUtleSI6NzcwNzcyOCwiY2xhc3NLZXkiOjIyMCwib3JkZXJLZXkiOjExNzYsImZhbWlseUtleSI6NzcxNywiZ2VudXNLZXkiOjI5Mjg5OTcsInNwZWNpZXNLZXkiOjI5MzAxMzcsImFjY2VwdGVkVGF4b25LZXkiOjI5MzAxMzcsInNjaWVudGlmaWNOYW1lIjoiU29sYW51bSBseWNvcGVyc2ljdW0gTC4iLCJhY2NlcHRlZFNjaWVudGlmaWNOYW1lIjoiU29sYW51bSBseWNvcGVyc2ljdW0gTC4iLCJraW5nZG9tIjoiUGxhbnRhZSIsInBoeWx1bSI6IlRyYWNoZW9waHl0YSIsIm9yZGVyIjoiU29sYW5hbGVzIiwiZmFtaWx5IjoiU29sYW5hY2VhZSIsImdlbnVzIjoiU29sYW51bSIsInNwZWNpZXMiOiJTb2xhbnVtIGx5Y29wZXJzaWN1bSIsImdlbmVyaWNOYW1lIjoiU29sYW51bSIsInNwZWNpZmljRXBpdGhldCI6Imx5Y29wZXJzaWN1bSIsInRheG9uUmFuayI6IlNQRUNJRVMiLCJ0YXhvbm9taWNTdGF0dXMiOiJBQ0NFUFRFRCIsIml1Y25SZWRMaXN0Q2F0ZWdvcnkiOiJORSIsImRhdGVJZGVudGlmaWVkIjoiMjAyNC0wMS0wMlQwNzozNjoxNyIsImRlY2ltYWxMYXRpdHVkZSI6MTcuMTYxMjg1LCJkZWNpbWFsTG9uZ2l0dWRlIjoxMDQuNzc0NTIxLCJjb250aW5lbnQiOiJBU0lBIiwic3RhdGVQcm92aW5jZSI6Ik5ha2hvbiBQaGFub20iLCJnYWRtIjp7ImxldmVsMCI6eyJnaWQiOiJUSEEiLCJuYW1lIjoiVGhhaWxhbmQifSwibGV2ZWwxIjp7ImdpZCI6IlRIQS4yOF8xIiwibmFtZSI6Ik5ha2hvbiBQaGFub20ifSwibGV2ZWwyIjp7ImdpZCI6IlRIQS4yOC4zXzEiLCJuYW1lIjoiTXVhbmcgTmFraG9uIFBoYW5vbSJ9LCJsZXZlbDMiOnsiZ2lkIjoiVEhBLjI4LjMuMl8xIiwibmFtZSI6IkJhbiBLbGFuZyJ9fSwieWVhciI6MjAyNCwibW9udGgiOjEsImRheSI6MiwiZXZlbnREYXRlIjoiMjAyNC0wMS0wMlQxNDozMTowNCIsImlzc3VlcyI6WyJDT09SRElOQVRFX1JPVU5ERUQiLCJDT05USU5FTlRfREVSSVZFRF9GUk9NX0NPT1JESU5BVEVTIiwiVEFYT05fTUFUQ0hfVEFYT05fSURfSUdOT1JFRCJdLCJtb2RpZmllZCI6IjIwMjQtMDEtMDJUMDc6NDA6MDUuMDAwKzAwOjAwIiwibGFzdEludGVycHJldGVkIjoiMjAyNC0wMS0wOVQxMzo0NToyNC45NjErMDA6MDAiLCJyZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL29ic2VydmF0aW9ucy8xOTU1NTE1NTkiLCJsaWNlbnNlIjoiaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMvNC4wL2xlZ2FsY29kZSIsImlkZW50aWZpZXJzIjpbeyJpZGVudGlmaWVyIjoiMTk1NTUxNTU5In1dLCJtZWRpYSI6W3sidHlwZSI6IlN0aWxsSW1hZ2UiLCJmb3JtYXQiOiJpbWFnZS9qcGVnIiwicmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9waG90b3MvMzQ0MDU4MTEzIiwiY3JlYXRlZCI6IjIwMjQtMDEtMDJUMDc6MzE6MDQuMDAwKzAwOjAwIiwiY3JlYXRvciI6Indhbndpc2EgXHVEODNEXHVEQzlFIiwicHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJsaWNlbnNlIjoiaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMvNC4wLyIsInJpZ2h0c0hvbGRlciI6Indhbndpc2EgXHVEODNEXHVEQzlFIiwiaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzM0NDA1ODExMy9vcmlnaW5hbC5qcGVnIn1dLCJmYWN0cyI6W10sInJlbGF0aW9ucyI6W10sImlzSW5DbHVzdGVyIjpmYWxzZSwiZGF0YXNldE5hbWUiOiJpTmF0dXJhbGlzdCByZXNlYXJjaC1ncmFkZSBvYnNlcnZhdGlvbnMiLCJyZWNvcmRlZEJ5Ijoid2Fud2lzYSBcdUQ4M0RcdURDOUUiLCJpZGVudGlmaWVkQnkiOiJ3YW53aXNhIFx1RDgzRFx1REM5RSIsImdlb2RldGljRGF0dW0iOiJXR1M4NCIsImNsYXNzIjoiTWFnbm9saW9wc2lkYSIsImNvdW50cnlDb2RlIjoiVEgiLCJyZWNvcmRlZEJ5SURzIjpbXSwiaWRlbnRpZmllZEJ5SURzIjpbXSwiY291bnRyeSI6IlRoYWlsYW5kIiwicmlnaHRzSG9sZGVyIjoid2Fud2lzYSBcdUQ4M0RcdURDOUUiLCJpZGVudGlmaWVyIjoiMTk1NTUxNTU5IiwiaHR0cDovL3Vua25vd24ub3JnL25pY2siOiJ3YW53aXNhX3BhcmVlc29pIiwidmVyYmF0aW1FdmVudERhdGUiOiIyMDI0LTAxLTAyIDE0OjMxOjA0IiwiZ2JpZklEIjoiNDUwNzk1MzM4NyIsInZlcmJhdGltTG9jYWxpdHkiOiI1UTZGK1dYSCDguJXguLPguJrguKUg4Lia4LmJ4Liy4LiZ4LiB4Lil4Liy4LiHIOC4reC4s+C5gOC4oOC4reC5gOC4oeC4t+C4reC4h+C4meC4hOC4o+C4nuC4meC4oSDguJnguITguKPguJ7guJnguKEgNDgwMDAg4Lib4Lij4Liw4LmA4LiX4Lio4LmE4LiX4LiiIiwiY29sbGVjdGlvbkNvZGUiOiJPYnNlcnZhdGlvbnMiLCJvY2N1cnJlbmNlSUQiOiJodHRwczovL3d3dy5pbmF0dXJhbGlzdC5vcmcvb2JzZXJ2YXRpb25zLzE5NTU1MTU1OSIsInRheG9uSUQiOiI1MTczNyIsImNhdGFsb2dOdW1iZXIiOiIxOTU1NTE1NTkiLCJpbnN0aXR1dGlvbkNvZGUiOiJpTmF0dXJhbGlzdCIsImV2ZW50VGltZSI6IjE0OjMxOjA0KzA3OjAwIiwiaHR0cDovL3Vua25vd24ub3JnL2NhcHRpdmUiOiJ3aWxkIiwiaWRlbnRpZmljYXRpb25JRCI6IjQzOTgyNzUzMiJ9LHsia2V5Ijo0MDExNjM3MjI4LCJkYXRhc2V0S2V5IjoiNTBjOTUwOWQtMjJjNy00YTIyLWE0N2QtOGM0ODQyNWVmNGE3IiwicHVibGlzaGluZ09yZ0tleSI6IjI4ZWIxYTNmLTFjMTUtNGE5NS05MzFhLTRhZjkwZWNiNTc0ZCIsImluc3RhbGxhdGlvbktleSI6Ijk5NzQ0OGE4LWY3NjItMTFlMS1hNDM5LTAwMTQ1ZWI0NWU5YSIsImhvc3RpbmdPcmdhbml6YXRpb25LZXkiOiIyOGViMWEzZi0xYzE1LTRhOTUtOTMxYS00YWY5MGVjYjU3NGQiLCJwdWJsaXNoaW5nQ291bnRyeSI6IkVTIiwicHJvdG9jb2wiOiJEV0NfQVJDSElWRSIsImxhc3RDcmF3bGVkIjoiMjAyNC0wMS0wOVQwMzozMzozOC43MzUrMDA6MDAiLCJsYXN0UGFyc2VkIjoiMjAyNC0wMS0wOVQxMzoxOToyMy4yMTArMDA6MDAiLCJjcmF3bElkIjo0MjYsImV4dGVuc2lvbnMiOnsiaHR0cDovL3JzLmdiaWYub3JnL3Rlcm1zLzEuMC9NdWx0aW1lZGlhIjpbeyJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRlZCI6IjIwMjMtMDEtMDJUMDk6MzU6MzMtMDg6MDAiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvbGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLzQuMC8iLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvdHlwZSI6IlN0aWxsSW1hZ2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvZm9ybWF0IjoiaW1hZ2UvanBlZyIsImh0dHA6Ly9ycy50ZHdnLm9yZy9kd2MvdGVybXMvY2F0YWxvZ051bWJlciI6IjI1MDIzMTQyOCIsImh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9yZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL3Bob3Rvcy8yNTAyMzE0MjgiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmlnaHRzSG9sZGVyIjoiZmFsdWtlIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2NyZWF0b3IiOiJmYWx1a2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzI1MDIzMTQyOC9vcmlnaW5hbC5qcGVnIn0seyJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvY3JlYXRlZCI6IjIwMjMtMDEtMDJUMDk6MzU6MTktMDg6MDAiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvbGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLzQuMC8iLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvdHlwZSI6IlN0aWxsSW1hZ2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvZm9ybWF0IjoiaW1hZ2UvanBlZyIsImh0dHA6Ly9ycy50ZHdnLm9yZy9kd2MvdGVybXMvY2F0YWxvZ051bWJlciI6IjI1MDIzMTQ3NCIsImh0dHA6Ly9wdXJsLm9yZy9kYy90ZXJtcy9yZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL3Bob3Rvcy8yNTAyMzE0NzQiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvcmlnaHRzSG9sZGVyIjoiZmFsdWtlIiwiaHR0cDovL3B1cmwub3JnL2RjL3Rlcm1zL2NyZWF0b3IiOiJmYWx1a2UiLCJodHRwOi8vcHVybC5vcmcvZGMvdGVybXMvaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzI1MDIzMTQ3NC9vcmlnaW5hbC5qcGVnIn1dfSwiYmFzaXNPZlJlY29yZCI6IkhVTUFOX09CU0VSVkFUSU9OIiwib2NjdXJyZW5jZVN0YXR1cyI6IlBSRVNFTlQiLCJ0YXhvbktleSI6MjkzMDEzNywia2luZ2RvbUtleSI6NiwicGh5bHVtS2V5Ijo3NzA3NzI4LCJjbGFzc0tleSI6MjIwLCJvcmRlcktleSI6MTE3NiwiZmFtaWx5S2V5Ijo3NzE3LCJnZW51c0tleSI6MjkyODk5Nywic3BlY2llc0tleSI6MjkzMDEzNywiYWNjZXB0ZWRUYXhvbktleSI6MjkzMDEzNywic2NpZW50aWZpY05hbWUiOiJTb2xhbnVtIGx5Y29wZXJzaWN1bSBMLiIsImFjY2VwdGVkU2NpZW50aWZpY05hbWUiOiJTb2xhbnVtIGx5Y29wZXJzaWN1bSBMLiIsImtpbmdkb20iOiJQbGFudGFlIiwicGh5bHVtIjoiVHJhY2hlb3BoeXRhIiwib3JkZXIiOiJTb2xhbmFsZXMiLCJmYW1pbHkiOiJTb2xhbmFjZWFlIiwiZ2VudXMiOiJTb2xhbnVtIiwic3BlY2llcyI6IlNvbGFudW0gbHljb3BlcnNpY3VtIiwiZ2VuZXJpY05hbWUiOiJTb2xhbnVtIiwic3BlY2lmaWNFcGl0aGV0IjoibHljb3BlcnNpY3VtIiwidGF4b25SYW5rIjoiU1BFQ0lFUyIsInRheG9ub21pY1N0YXR1cyI6IkFDQ0VQVEVEIiwiaXVjblJlZExpc3RDYXRlZ29yeSI6Ik5FIiwiZGF0ZUlkZW50aWZpZWQiOiIyMDIzLTAxLTAyVDEwOjA4OjIxIiwiZGVjaW1hbExhdGl0dWRlIjozNi44MDc2ODUsImRlY2ltYWxMb25naXR1ZGUiOi0yLjY1ODM2NSwiY29vcmRpbmF0ZVVuY2VydGFpbnR5SW5NZXRlcnMiOjIuMCwiY29udGluZW50IjoiRVVST1BFIiwic3RhdGVQcm92aW5jZSI6IkFuZGFsdWPDrWEiLCJnYWRtIjp7ImxldmVsMCI6eyJnaWQiOiJFU1AiLCJuYW1lIjoiU3BhaW4ifSwibGV2ZWwxIjp7ImdpZCI6IkVTUC4xXzEiLCJuYW1lIjoiQW5kYWx1Y8OtYSJ9LCJsZXZlbDIiOnsiZ2lkIjoiRVNQLjEuMV8xIiwibmFtZSI6IkFsbWVyw61hIn0sImxldmVsMyI6eyJnaWQiOiJFU1AuMS4xLjZfMSIsIm5hbWUiOiJuLmEuICgyMCkifX0sInllYXIiOjIwMjMsIm1vbnRoIjoxLCJkYXkiOjIsImV2ZW50RGF0ZSI6IjIwMjMtMDEtMDJUMDk6MzU6MDAiLCJpc3N1ZXMiOlsiQ09PUkRJTkFURV9ST1VOREVEIiwiQ09OVElORU5UX0RFUklWRURfRlJPTV9DT09SRElOQVRFUyIsIlRBWE9OX01BVENIX1RBWE9OX0lEX0lHTk9SRUQiXSwibW9kaWZpZWQiOiIyMDIzLTAzLTE3VDIyOjE2OjE5LjAwMCswMDowMCIsImxhc3RJbnRlcnByZXRlZCI6IjIwMjQtMDEtMDlUMTM6MTk6MjMuMjEwKzAwOjAwIiwicmVmZXJlbmNlcyI6Imh0dHBzOi8vd3d3LmluYXR1cmFsaXN0Lm9yZy9vYnNlcnZhdGlvbnMvMTQ1NjUzMjQ2IiwibGljZW5zZSI6Imh0dHA6Ly9jcmVhdGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLzQuMC9sZWdhbGNvZGUiLCJpZGVudGlmaWVycyI6W3siaWRlbnRpZmllciI6IjE0NTY1MzI0NiJ9XSwibWVkaWEiOlt7InR5cGUiOiJTdGlsbEltYWdlIiwiZm9ybWF0IjoiaW1hZ2UvanBlZyIsInJlZmVyZW5jZXMiOiJodHRwczovL3d3dy5pbmF0dXJhbGlzdC5vcmcvcGhvdG9zLzI1MDIzMTQ3NCIsImNyZWF0ZWQiOiIyMDIzLTAxLTAyVDE3OjM1OjE5LjAwMCswMDowMCIsImNyZWF0b3IiOiJmYWx1a2UiLCJwdWJsaXNoZXIiOiJpTmF0dXJhbGlzdCIsImxpY2Vuc2UiOiJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS1uYy80LjAvIiwicmlnaHRzSG9sZGVyIjoiZmFsdWtlIiwiaWRlbnRpZmllciI6Imh0dHBzOi8vaW5hdHVyYWxpc3Qtb3Blbi1kYXRhLnMzLmFtYXpvbmF3cy5jb20vcGhvdG9zLzI1MDIzMTQ3NC9vcmlnaW5hbC5qcGVnIn0seyJ0eXBlIjoiU3RpbGxJbWFnZSIsImZvcm1hdCI6ImltYWdlL2pwZWciLCJyZWZlcmVuY2VzIjoiaHR0cHM6Ly93d3cuaW5hdHVyYWxpc3Qub3JnL3Bob3Rvcy8yNTAyMzE0MjgiLCJjcmVhdGVkIjoiMjAyMy0wMS0wMlQxNzozNTozMy4wMDArMDA6MDAiLCJjcmVhdG9yIjoiZmFsdWtlIiwicHVibGlzaGVyIjoiaU5hdHVyYWxpc3QiLCJsaWNlbnNlIjoiaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbGljZW5zZXMvYnktbmMvNC4wLyIsInJpZ2h0c0hvbGRlciI6ImZhbHVrZSIsImlkZW50aWZpZXIiOiJodHRwczovL2luYXR1cmFsaXN0LW9wZW4tZGF0YS5zMy5hbWF6b25hd3MuY29tL3Bob3Rvcy8yNTAyMzE0Mjgvb3JpZ2luYWwuanBlZyJ9XSwiZmFjdHMiOltdLCJyZWxhdGlvbnMiOltdLCJpc0luQ2x1c3RlciI6ZmFsc2UsImRhdGFzZXROYW1lIjoiaU5hdHVyYWxpc3QgcmVzZWFyY2gtZ3JhZGUgb2JzZXJ2YXRpb25zIiwicmVjb3JkZWRCeSI6ImZhbHVrZSIsImlkZW50aWZpZWRCeSI6ImZhbHVrZSIsImdlb2RldGljRGF0dW0iOiJXR1M4NCIsImNsYXNzIjoiTWFnbm9saW9wc2lkYSIsImNvdW50cnlDb2RlIjoiRVMiLCJyZWNvcmRlZEJ5SURzIjpbXSwiaWRlbnRpZmllZEJ5SURzIjpbXSwiY291bnRyeSI6IlNwYWluIiwicmlnaHRzSG9sZGVyIjoiZmFsdWtlIiwiaWRlbnRpZmllciI6IjE0NTY1MzI0NiIsImh0dHA6Ly91bmtub3duLm9yZy9uaWNrIjoiZmFsdWtlIiwidmVyYmF0aW1FdmVudERhdGUiOiIyMDIzLzAxLzAyIDk6MzUgQU0iLCJnYmlmSUQiOiI0MDExNjM3MjI4IiwidmVyYmF0aW1Mb2NhbGl0eSI6IkFsbWVyw61hLCBFc3Bhw7FhIiwiY29sbGVjdGlvbkNvZGUiOiJPYnNlcnZhdGlvbnMiLCJvY2N1cnJlbmNlSUQiOiJodHRwczovL3d3dy5pbmF0dXJhbGlzdC5vcmcvb2JzZXJ2YXRpb25zLzE0NTY1MzI0NiIsInRheG9uSUQiOiI1MTczNyIsImNhdGFsb2dOdW1iZXIiOiIxNDU2NTMyNDYiLCJpbnN0aXR1dGlvbkNvZGUiOiJpTmF0dXJhbGlzdCIsImV2ZW50VGltZSI6IjA5OjM1OjAwKzAxOjAwIiwicmVwcm9kdWN0aXZlQ29uZGl0aW9uIjoiZnJ1aXRpbmciLCJodHRwOi8vdW5rbm93bi5vcmcvY2FwdGl2ZSI6IndpbGQiLCJpZGVudGlmaWNhdGlvbklEIjoiMzI0MzQ3MTEyIn1dLCJmYWNldHMiOltdfQ== + recorded_at: Sun, 14 Jan 2024 10:17:48 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/factories/planting.rb b/spec/factories/planting.rb index 8709dd32a..815d349c6 100644 --- a/spec/factories/planting.rb +++ b/spec/factories/planting.rb @@ -49,7 +49,7 @@ FactoryBot.define do crop { FactoryBot.create(:perennial_crop) } end - factory :predicatable_planting do + factory :predictable_planting do crop do crop = FactoryBot.create(:annual_crop) FactoryBot.create(:planting, crop:, planted_at: 10.days.ago) diff --git a/spec/factories/scientific_name.rb b/spec/factories/scientific_name.rb index ec43799ce..a8363397b 100644 --- a/spec/factories/scientific_name.rb +++ b/spec/factories/scientific_name.rb @@ -2,7 +2,7 @@ FactoryBot.define do factory :scientific_name do - association :crop, factory: :crop + association :crop name { "Beanus Magicus" } creator diff --git a/spec/features/admin/admin_spec.rb b/spec/features/admin/admin_spec.rb index a4f97ac22..e4cd871d1 100644 --- a/spec/features/admin/admin_spec.rb +++ b/spec/features/admin/admin_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "forums", js: true do +describe "forums", :js do context 'signed in admin' do include_context 'signed in admin' it "navigating to forum admin with js" do diff --git a/spec/features/admin/forums_spec.rb b/spec/features/admin/forums_spec.rb index fb90a9d5b..5c0847cdb 100644 --- a/spec/features/admin/forums_spec.rb +++ b/spec/features/admin/forums_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "forums", js: true do +describe "forums", :js do include_context 'signed in admin' let(:forum) { create(:forum) } diff --git a/spec/features/admin/newsletter_spec.rb b/spec/features/admin/newsletter_spec.rb new file mode 100644 index 000000000..42023662b --- /dev/null +++ b/spec/features/admin/newsletter_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe "newsletter subscribers", :js do + include_context 'signed in admin' + + let(:subscriber) { create(:newsletter_recipient_member) } + + describe "navigating to newsletter subscribers admin with js" do + before do + @subscriber = subscriber + visit admin_path + within 'nav#member_admin' do + click_link "Newsletter subscribers" + end + end + + it { expect(page).to have_current_path admin_newsletter_path, ignore_query: true } + it { expect(page).to have_content @subscriber.email } + end +end \ No newline at end of file diff --git a/spec/features/admin/plant_parts_spec.rb b/spec/features/admin/plant_parts_spec.rb new file mode 100644 index 000000000..300929982 --- /dev/null +++ b/spec/features/admin/plant_parts_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe "plant parts", :js do + include_context 'signed in admin' + + let(:plant_part) { create(:plant_part) } + + describe "navigating to plant parts admin with js" do + before do + visit admin_path + within 'nav#crop_admin' do + click_link "Plant parts" + end + end + + it { expect(page).to have_current_path plant_parts_path, ignore_query: true } + it { expect(page).to have_link "New plant part" } + end + + describe "adding a plant part" do + before do + visit plant_parts_path + click_link "New plant part" + expect(page).to have_current_path new_plant_part_path, ignore_query: true + fill_in 'Name', with: "this is a new plant part" + click_button 'Save' + end + + it { expect(page).to have_current_path plant_part_path(PlantPart.last), ignore_query: true } + it { expect(page).to have_content 'Plant part was successfully created' } + end + + describe 'editing plant part' do + before do + @plant_part = plant_part + visit plant_parts_path + click_link 'Edit', href: edit_plant_part_path(@plant_part) + fill_in 'Name', with: 'Something else' + click_button 'Save' + plant_part.reload + end + + it { expect(page).to have_current_path plant_part_path(@plant_part), ignore_query: true } + it { expect(page).to have_content 'Plant part was successfully updated' } + it { expect(page).to have_content 'Something Else' } + end + + describe 'deleting plant part' do + before do + @plant_part = plant_part + visit plant_parts_path + accept_confirm do + click_link 'Delete', href: plant_part_path(@plant_part) + end + end + + it { expect(page).to have_current_path plant_parts_path, ignore_query: true } + it { expect(page).to have_content 'Plant part was successfully destroyed' } + end +end diff --git a/spec/features/admin/roles_spec.rb b/spec/features/admin/roles_spec.rb new file mode 100644 index 000000000..2c9ef79ba --- /dev/null +++ b/spec/features/admin/roles_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe "roles", :js do + include_context 'signed in admin' + + let(:role) { create(:role) } + + describe "navigating to roles admin with js" do + before do + visit admin_path + within 'nav#site_admin' do + click_link "Roles" + end + end + + it { expect(page).to have_current_path admin_roles_path, ignore_query: true } + it { expect(page).to have_link "New role" } + end + + describe "adding a role" do + before do + visit admin_roles_path + click_link "New role" + expect(page).to have_current_path new_admin_role_path, ignore_query: true + fill_in 'Name', with: 'Discussion' + fill_in 'Description', with: "this is a new role" + click_button 'Save' + end + + it { expect(page).to have_current_path admin_roles_path, ignore_query: true } + it { expect(page).to have_content 'Role was successfully created' } + end + + describe 'editing role' do + before do + @role = role + visit admin_roles_path + click_link 'Edit', href: edit_admin_role_path(@role) + fill_in 'Description', with: 'Something else' + click_button 'Save' + role.reload + end + + it { expect(page).to have_current_path admin_roles_path, ignore_query: true } + it { expect(page).to have_content 'Role was successfully updated' } + it { expect(page).to have_content 'Something else' } + end + + describe 'deleting role' do + before do + @role = role + visit admin_roles_path + accept_confirm do + click_link 'Delete', href: admin_role_path(@role) + end + end + + it { expect(page).to have_current_path admin_roles_path, ignore_query: true } + it { expect(page).to have_content 'Role was successfully deleted' } + end +end diff --git a/spec/features/comments/commenting_a_comment_spec.rb b/spec/features/comments/commenting_a_comment_spec.rb index 64121a44e..4ff6f2b11 100644 --- a/spec/features/comments/commenting_a_comment_spec.rb +++ b/spec/features/comments/commenting_a_comment_spec.rb @@ -9,6 +9,8 @@ describe 'Commenting on a post' do before { visit new_comment_path post_id: post.id } + include_examples 'is accessible' + it "creating a comment" do fill_in "comment_body", with: "This is a sample test for comment" click_button "Post comment" @@ -24,6 +26,8 @@ describe 'Commenting on a post' do visit edit_comment_path existing_comment end + include_examples 'is accessible' + it "saving edit" do fill_in "comment_body", with: "Testing edit for comment" click_button "Post comment" diff --git a/spec/features/conversations/index_spec.rb b/spec/features/conversations/index_spec.rb index d2edcb4f7..9fd2d4cff 100644 --- a/spec/features/conversations/index_spec.rb +++ b/spec/features/conversations/index_spec.rb @@ -18,6 +18,8 @@ describe "Conversations", :js do click_link 'Inbox' end + include_examples 'is accessible' + it { expect(page).to have_content 'something i want to say' } it { page.percy_snapshot(page, name: 'conversations#index') } diff --git a/spec/features/crops/alternate_name_spec.rb b/spec/features/crops/alternate_name_spec.rb index 8d9882daf..380a7f71d 100644 --- a/spec/features/crops/alternate_name_spec.rb +++ b/spec/features/crops/alternate_name_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "Alternate names", js: true do +describe "Alternate names", :js do let!(:alternate_eggplant) { create(:alternate_eggplant) } let(:crop) { alternate_eggplant.crop } diff --git a/spec/features/crops/creating_a_crop_spec.rb b/spec/features/crops/creating_a_crop_spec.rb index b4bb23e1b..afaaad7e2 100644 --- a/spec/features/crops/creating_a_crop_spec.rb +++ b/spec/features/crops/creating_a_crop_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "Crop", js: true do +describe "Crop", :js do shared_context 'fill in form' do before do visit new_crop_path @@ -22,6 +22,7 @@ describe "Crop", js: true do end end end + shared_examples 'request crop' do describe "requesting a crop with multiple scientific and alternate name" do include_examples 'fill in form' @@ -38,6 +39,7 @@ describe "Crop", js: true do it { expect(page).to have_content "Matsurika" } end end + shared_examples 'create crop' do describe "creating a crop with multiple scientific and alternate name" do include_examples 'fill in form' diff --git a/spec/features/crops/crop_detail_page_spec.rb b/spec/features/crops/crop_detail_page_spec.rb index 41e9126d5..b441bd7fe 100644 --- a/spec/features/crops/crop_detail_page_spec.rb +++ b/spec/features/crops/crop_detail_page_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "crop detail page", js: true do +describe "crop detail page", :js do subject do # Update the medians after all the # data has been loaded diff --git a/spec/features/crops/crop_wranglers_spec.rb b/spec/features/crops/crop_wranglers_spec.rb index 31949fcd8..7307d6e6a 100644 --- a/spec/features/crops/crop_wranglers_spec.rb +++ b/spec/features/crops/crop_wranglers_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "crop wranglers", js: true do +describe "crop wranglers", :js do context "signed in wrangler" do include_context 'signed in crop wrangler' let!(:crop_wranglers) { create_list(:crop_wrangling_member, 3) } diff --git a/spec/features/crops/scientific_name_spec.rb b/spec/features/crops/scientific_name_spec.rb index ccf2b2bbe..ac437bc05 100644 --- a/spec/features/crops/scientific_name_spec.rb +++ b/spec/features/crops/scientific_name_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "Scientific names", js: true do +describe "Scientific names", :js do let!(:zea_mays) { create(:zea_mays) } let(:crop) { zea_mays.crop } diff --git a/spec/features/footer_spec.rb b/spec/features/footer_spec.rb index 6e5713f62..147c68a6b 100644 --- a/spec/features/footer_spec.rb +++ b/spec/features/footer_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "footer", js: true do +describe "footer", :js do before { visit root_path } it "footer is on home page" do diff --git a/spec/features/gardens/adding_gardens_spec.rb b/spec/features/gardens/adding_gardens_spec.rb index d281e9657..f92a7e3d1 100644 --- a/spec/features/gardens/adding_gardens_spec.rb +++ b/spec/features/gardens/adding_gardens_spec.rb @@ -8,6 +8,8 @@ describe "Gardens", :js do include_context 'signed in member' before { visit new_garden_path } + include_examples 'is accessible' + it "has the required fields help text" do expect(page).to have_content "* denotes a required field" end diff --git a/spec/features/gardens/gardens_spec.rb b/spec/features/gardens/gardens_spec.rb index 8d3170b73..cc9a8842f 100644 --- a/spec/features/gardens/gardens_spec.rb +++ b/spec/features/gardens/gardens_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "Planting a crop", js: true do +describe "Planting a crop", :js do context 'signed in' do include_context 'signed in member' # name is aaa to ensure it is ordered first @@ -14,7 +14,7 @@ describe "Planting a crop", js: true do it "View gardens" do visit gardens_path expect(page).to have_content "Everyone's gardens" - click_link "My gardens" + click_link "My gardens", match: :first expect(page).to have_content "#{garden.owner.login_name}'s gardens" click_link "Everyone's gardens" expect(page).to have_content "Everyone's gardens" @@ -127,4 +127,6 @@ describe "Planting a crop", js: true do expect(page).not_to have_content finished_planting.crop_name end end + + # TODO: include_examples 'is accessible' end diff --git a/spec/features/gardens/index_spec.rb b/spec/features/gardens/index_spec.rb index 8c927be37..443fc3cce 100644 --- a/spec/features/gardens/index_spec.rb +++ b/spec/features/gardens/index_spec.rb @@ -14,6 +14,8 @@ describe "Gardens#index", :js do visit member_gardens_path(member_slug: member.slug) end + include_examples 'is accessible' + it "displays each of the gardens" do member.gardens.each do |garden| expect(page).to have_text garden.name diff --git a/spec/features/home/home_spec.rb b/spec/features/home/home_spec.rb index ca58638d9..f6e6a9d46 100644 --- a/spec/features/home/home_spec.rb +++ b/spec/features/home/home_spec.rb @@ -39,11 +39,11 @@ describe "home page", :search do end it "does not show finished seeds" do - expect(subject).not_to have_link href: seed_path(finished_seed) + expect(subject).to have_no_link href: seed_path(finished_seed) end it "does not show untradable seeds" do - expect(subject).not_to have_link href: seed_path(untradable_seed) + expect(subject).to have_no_link href: seed_path(untradable_seed) end it { is_expected.to have_link 'View all seeds ยป' } @@ -89,6 +89,7 @@ describe "home page", :search do include_examples 'show plantings' include_examples 'show harvests' include_examples 'shows seeds' + include_examples 'is accessible' it { is_expected.to have_text 'community of food gardeners' } end @@ -98,6 +99,7 @@ describe "home page", :search do include_examples 'show plantings' include_examples 'show harvests' include_examples 'shows seeds' + include_examples 'is accessible' describe 'should say welcome' do before { visit root_path } diff --git a/spec/features/likeable_spec.rb b/spec/features/likeable_spec.rb index da082ee29..adf4397fe 100644 --- a/spec/features/likeable_spec.rb +++ b/spec/features/likeable_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe 'Likeable', :js, search: true do +describe 'Likeable', :js, :search do let(:another_member) { FactoryBot.create(:london_member) } let!(:post) { FactoryBot.create(:post, :reindex, author: member) } let!(:photo) { FactoryBot.create(:photo, :reindex, owner: member) } @@ -51,6 +51,7 @@ describe 'Likeable', :js, search: true do logout(another_member) end end + describe 'photos#index' do let(:path) { photos_path } diff --git a/spec/features/locale_spec.rb b/spec/features/locale_spec.rb index 093aad6c1..db3683710 100644 --- a/spec/features/locale_spec.rb +++ b/spec/features/locale_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "Changing locales", js: true do +describe "Changing locales", :js do after { I18n.locale = :en } let(:member) { FactoryBot.create(:member) } diff --git a/spec/features/members/profile_spec.rb b/spec/features/members/profile_spec.rb index c5978b890..5e206d3d7 100644 --- a/spec/features/members/profile_spec.rb +++ b/spec/features/members/profile_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "member profile", js: true do +describe "member profile", :js do let(:member) { create(:member) } let(:other_member) { create(:member) } let(:admin_member) { create(:admin_member) } diff --git a/spec/features/percy/percy_spec.rb b/spec/features/percy/percy_spec.rb index 23cf28afc..066c48102 100644 --- a/spec/features/percy/percy_spec.rb +++ b/spec/features/percy/percy_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe 'Test with visual testing', js: true, type: :feature do +describe 'Test with visual testing', :js, type: :feature do # Use the same random seed every time so our random data is the same # on every run, so doesn't trigger percy to see changes before { Faker::Config.random = Random.new(42) } diff --git a/spec/features/planting_reminder_spec.rb b/spec/features/planting_reminder_spec.rb index 5e5adab69..7b2a5bec0 100644 --- a/spec/features/planting_reminder_spec.rb +++ b/spec/features/planting_reminder_spec.rb @@ -30,8 +30,8 @@ describe "Planting reminder email", :js do context "when member has some plantings" do # Bangs are used on the following 2 let blocks in order to ensure that the plantings are present # in the database before the email is generated: otherwise, they won't be present in the email. - let!(:p1) { FactoryBot.create(:predicatable_planting, planted_at: 10.days.ago, garden: member.gardens.first, owner: member) } - let!(:p2) { FactoryBot.create(:predicatable_planting, planted_at: 30.days.ago, garden: member.gardens.first, owner: member) } + let!(:p1) { FactoryBot.create(:predictable_planting, planted_at: 10.days.ago, garden: member.gardens.first, owner: member) } + let!(:p2) { FactoryBot.create(:predictable_planting, planted_at: 30.days.ago, garden: member.gardens.first, owner: member) } describe "lists plantings" do it { expect(mail).to have_content "Progress report" } @@ -50,8 +50,8 @@ describe "Planting reminder email", :js do context "when member has some harvests" do # Bangs are used on the following 2 let blocks in order to ensure that the plantings are present # in the database before the spec is run. - let!(:p1) { FactoryBot.create(:predicatable_planting, garden: member.gardens.first, owner: member, planted_at: 20.days.ago) } - let!(:p2) { FactoryBot.create(:predicatable_planting, garden: member.gardens.first, owner: member) } + let!(:p1) { FactoryBot.create(:predictable_planting, garden: member.gardens.first, owner: member, planted_at: 20.days.ago) } + let!(:p2) { FactoryBot.create(:predictable_planting, garden: member.gardens.first, owner: member) } let!(:h1) { FactoryBot.create(:harvest, owner: member, planting: p1, harvested_at: 1.day.ago) } let!(:h2) { FactoryBot.create(:harvest, owner: member, planting: p2, harvested_at: 3.days.ago) } diff --git a/spec/features/plantings/show_spec.rb b/spec/features/plantings/show_spec.rb index 8d8c8a396..8731d218b 100644 --- a/spec/features/plantings/show_spec.rb +++ b/spec/features/plantings/show_spec.rb @@ -26,7 +26,7 @@ describe "Display a planting", :js do end context 'Annual with predicted finish' do - let(:planting) { FactoryBot.create(:predicatable_planting, planted_at: 2.weeks.ago) } + let(:planting) { FactoryBot.create(:predictable_planting, planted_at: 2.weeks.ago) } it { expect(page).to have_text '28%' } it { expect(page).to have_text '14/50 days' } diff --git a/spec/features/seeds/adding_seeds_spec.rb b/spec/features/seeds/adding_seeds_spec.rb index f3ea7bf0e..7dc8967d2 100644 --- a/spec/features/seeds/adding_seeds_spec.rb +++ b/spec/features/seeds/adding_seeds_spec.rb @@ -17,19 +17,21 @@ describe "Seeds", :js, :search do end describe "displays required and optional fields properly" do - it { expect(page).to have_selector ".form-group.required", text: "Crop" } + # Note: The required behaviour is pushed down to the control itself, not the form-group as of rails 7.1. + # Modern browsers enforce the required behaviour better than us doing it ourselves. + it { expect(page).to have_selector "label", text: "Crop" } it { expect(page).to have_selector 'input#seed_quantity' } it { expect(page).to have_selector 'input#seed_plant_before' } it { expect(page).to have_selector 'input#seed_days_until_maturity_min' } it { expect(page).to have_selector 'input#seed_days_until_maturity_max' } - it { expect(page).to have_selector '.form-group.required', text: 'Organic?' } - it { expect(page).to have_selector '.form-group.required', text: 'GMO?' } - it { expect(page).to have_selector '.form-group.required', text: 'Heirloom?' } + it { expect(page).to have_selector 'label', text: 'Organic?' } + it { expect(page).to have_selector 'label', text: 'GMO?' } + it { expect(page).to have_selector 'label', text: 'Heirloom?' } it { expect(page).to have_selector 'textarea#seed_description' } - it { expect(page).to have_selector '.form-group.required', text: 'Will trade' } + it { expect(page).to have_selector 'label', text: 'Will trade' } end - describe "Adding a new seed", js: true do + describe "Adding a new seed", :js do before do fill_autocomplete "crop", with: "mai" select_from_autocomplete "maize" diff --git a/spec/features/seeds/misc_seeds_spec.rb b/spec/features/seeds/misc_seeds_spec.rb index bbe357d25..24a8b3a78 100644 --- a/spec/features/seeds/misc_seeds_spec.rb +++ b/spec/features/seeds/misc_seeds_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "seeds", js: true do +describe "seeds", :js do context "signed in user" do include_context 'signed in member' xit "button on index to edit seed" do diff --git a/spec/features/signin_spec.rb b/spec/features/signin_spec.rb index aa9082bd3..e0680b191 100644 --- a/spec/features/signin_spec.rb +++ b/spec/features/signin_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "signin", js: true do +describe "signin", :js do let(:member) { FactoryBot.create(:member) } let(:recipient) { FactoryBot.create(:member) } let(:wrangler) { FactoryBot.create(:crop_wrangling_member) } diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index 5dabbb02d..7745370bc 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "signup", js: true do +describe "signup", :js do it "sign up for new account from top menubar" do visit crops_path # something other than front page, which has multiple signup links click_link 'Sign up' diff --git a/spec/features/timeline/index_spec.rb b/spec/features/timeline/index_spec.rb index 53d02879b..b7db4d4d5 100644 --- a/spec/features/timeline/index_spec.rb +++ b/spec/features/timeline/index_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "timeline", js: true do +describe "timeline", :js do let(:member) { FactoryBot.create(:member) } let(:friend1) { FactoryBot.create(:member) } let(:friend2) { FactoryBot.create(:member) } diff --git a/spec/lib/haml/filters/escaped_markdown_spec.rb b/spec/lib/haml/filters/escaped_markdown_spec.rb index fd9b4e0b4..c5dda24c1 100644 --- a/spec/lib/haml/filters/escaped_markdown_spec.rb +++ b/spec/lib/haml/filters/escaped_markdown_spec.rb @@ -7,18 +7,32 @@ require 'haml/helpers' describe 'Haml::Filters::Escaped_Markdown' do it 'is registered as the handler for :escaped_markdown' do - Haml::Filters.defined['escaped_markdown'].should == + Haml::Filters.registered[:escaped_markdown].should == Haml::Filters::EscapedMarkdown end it 'converts Markdown to escaped HTML' do - rendered = Haml::Filters::EscapedMarkdown.render("**foo**") - rendered.should == "<p><strong>foo</strong></p>" + template = <<~HTML + :escaped_markdown + **foo** + HTML + rendered = render_haml(template) + expect(rendered).to eq "<p><strong>foo</strong></p>\n\n" end it 'converts quick crop links' do @crop = FactoryBot.create(:crop) - rendered = Haml::Filters::EscapedMarkdown.render("[#{@crop.name}](crop)") - rendered.should match(/<a href="/) + template = <<~HTML + :escaped_markdown + [#{@crop.name}](crop) + HTML + rendered = render_haml(template) + expect(rendered).to match(/<a href="/) + end + + def render_haml(haml) + locals = {} + options = {} + Haml::Template.new(options) { haml }.render(Object.new, locals) end end diff --git a/spec/lib/haml/filters/growstuff_markdown_spec.rb b/spec/lib/haml/filters/growstuff_markdown_spec.rb index cc27bd560..5c242156b 100644 --- a/spec/lib/haml/filters/growstuff_markdown_spec.rb +++ b/spec/lib/haml/filters/growstuff_markdown_spec.rb @@ -26,24 +26,24 @@ end describe 'Haml::Filters::Growstuff_Markdown' do it 'is registered as the handler for :growstuff_markdown' do - Haml::Filters.defined['growstuff_markdown'].should == + Haml::Filters.registered[:growstuff_markdown].should == Haml::Filters::GrowstuffMarkdown end it 'converts quick crop links' do @crop = FactoryBot.create(:crop) - rendered = Haml::Filters::GrowstuffMarkdown.render(input_link(@crop.name)) + rendered = render_haml(haml_template(input_link(@crop.name))) expect(rendered).to match(/#{output_link(@crop)}/) end it "doesn't convert nonexistent crops" do - rendered = Haml::Filters::GrowstuffMarkdown.render(input_link("not a crop")) + rendered = render_haml(haml_template(input_link("not a crop"))) expect(rendered).to match(/not a crop/) end it "doesn't convert escaped crop links" do @crop = FactoryBot.create(:crop) - rendered = Haml::Filters::GrowstuffMarkdown.render("\\" << input_link(@crop.name)) + rendered = render_haml(haml_template("\\" << input_link(@crop.name))) expect(rendered).to match(/\[#{@crop.name}\]\(crop\)/) end @@ -51,55 +51,56 @@ describe 'Haml::Filters::Growstuff_Markdown' do tomato = FactoryBot.create(:tomato) maize = FactoryBot.create(:maize) string = "#{input_link(tomato)} #{input_link(maize)}" - rendered = Haml::Filters::GrowstuffMarkdown.render(string) + rendered = render_haml(haml_template(string)) expect(rendered).to match(/#{output_link(tomato)} #{output_link(maize)}/) end it "converts normal markdown" do string = "**foo**" - rendered = Haml::Filters::GrowstuffMarkdown.render(string) + rendered = render_haml(haml_template(string)) expect(rendered).to match(%r{foo}) end it "finds crops case insensitively" do @crop = FactoryBot.create(:crop, name: 'tomato', slug: 'tomato') - rendered = Haml::Filters::GrowstuffMarkdown.render(input_link('ToMaTo')) + rendered = render_haml(haml_template(input_link('ToMaTo'))) expect(rendered).to match(/#{output_link(@crop, 'ToMaTo')}/) end it "fixes PT bug #78615258 (Markdown rendering bug with URLs and crops in same text)" do tomato = FactoryBot.create(:tomato) string = "[test](http://example.com) [tomato](crop)" - rendered = Haml::Filters::GrowstuffMarkdown.render(string) + rendered = render_haml(haml_template(string)) + expect(rendered).to match(/#{output_link(tomato)}/) expect(rendered).to match "test" end it 'converts quick member links' do @member = FactoryBot.create(:member) - rendered = Haml::Filters::GrowstuffMarkdown.render(input_member_link(@member.login_name)) + rendered = render_haml(haml_template(input_member_link(@member.login_name))) expect(rendered).to match(/#{output_member_link(@member)}/) end it "doesn't convert nonexistent members" do - rendered = Haml::Filters::GrowstuffMarkdown.render(input_member_link("not a member")) + rendered = render_haml(haml_template(input_member_link("not a member"))) expect(rendered).to include('not a member') end it "doesn't convert escaped members" do @member = FactoryBot.create(:member) - rendered = Haml::Filters::GrowstuffMarkdown.render("\\" << input_member_link(@member.login_name)) + rendered = render_haml(haml_template("\\" << input_member_link(@member.login_name))) expect(rendered).to match(/\[#{@member.login_name}\]\(member\)/) end it 'converts @ member links' do @member = FactoryBot.create(:member) - rendered = Haml::Filters::GrowstuffMarkdown.render("Hey @#{@member.login_name}! What's up") + rendered = render_haml(haml_template("Hey @#{@member.login_name}! What's up")) expect(rendered).to match(/#{output_member_link(@member, "@#{@member.login_name}")}/) end it "doesn't convert invalid @ members" do - rendered = Haml::Filters::GrowstuffMarkdown.render("@notamember") + rendered = render_haml(haml_template("@notamember")) expect(rendered).to include('@notamember') end @@ -107,13 +108,26 @@ describe 'Haml::Filters::Growstuff_Markdown' do @member = FactoryBot.create(:member) @member_name = @member.login_name @member.destroy - rendered = Haml::Filters::GrowstuffMarkdown.render("Hey @#{@member_name}") + rendered = render_haml(haml_template("Hey @#{@member_name}")) expect(rendered).to include("Hey @#{@member_name}") end it "doesn't convert escaped @ members" do @member = FactoryBot.create(:member) - rendered = Haml::Filters::GrowstuffMarkdown.render("Hey \\@#{@member.login_name}! What's up") + rendered = render_haml(haml_template("Hey \\@#{@member.login_name}! What's up")) expect(rendered).to include("Hey @#{@member.login_name}!") end + + def haml_template(input) + <<~HTML + :growstuff_markdown + #{input} + HTML + end + + def render_haml(haml) + locals = {} + options = {} + Haml::Template.new(options) { haml }.render(Object.new, locals) + end end diff --git a/spec/models/crop_spec.rb b/spec/models/crop_spec.rb index 423c86ade..9dd9bc3b2 100644 --- a/spec/models/crop_spec.rb +++ b/spec/models/crop_spec.rb @@ -143,6 +143,7 @@ describe Crop do shared_examples 'has default photo' do it { expect(described_class.has_photos).to include(crop) } end + let!(:crop) { FactoryBot.create(:tomato) } context 'with a planting photo' do diff --git a/spec/models/garden_type.rb b/spec/models/garden_type_spec.rb similarity index 100% rename from spec/models/garden_type.rb rename to spec/models/garden_type_spec.rb diff --git a/spec/models/photo_spec.rb b/spec/models/photo_spec.rb index 1c3adc07f..ffa37c595 100644 --- a/spec/models/photo_spec.rb +++ b/spec/models/photo_spec.rb @@ -115,7 +115,7 @@ describe Photo do planting.photos << photo harvest.destroy # photo is now used by harvest but not planting photo.destroy_if_unused - expect{ photo.reload }.not_to raise_error + expect { photo.reload }.not_to raise_error end it 'they are used by harvests but not plantings' do @@ -237,7 +237,7 @@ describe Photo do end end - describe 'Elastic search indexing', search: true do + describe 'Elastic search indexing', :search do let!(:planting) { FactoryBot.create(:planting, :reindex, owner: photo.owner) } let!(:crop) { FactoryBot.create(:crop, :reindex) } diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 37bf1aee3..4353bcc60 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -20,22 +20,49 @@ require 'capybara' require 'capybara/rspec' require 'selenium/webdriver' require 'capybara-screenshot/rspec' +require 'axe-capybara' +require 'axe-rspec' -require 'webdrivers' +# TODO: We may want to trial options.add_argument('--disable-dev-shm-usage') ### optional -Capybara.default_driver = :selenium_chrome_headless -Capybara.javascript_driver = :selenium_chrome_headless +# Required for running in the dev container +Capybara.register_driver :selenium_chrome_customised_headless do |app| + options = Selenium::WebDriver::Options.chrome + options.add_argument("--headless") + options.add_argument("--no-sandbox") + # driver = Selenium::WebDriver.for :chrome, options: options + + Capybara::Selenium::Driver.new(app, browser: :chrome, options:) +end + +# Ability to pass in flags to +if ENV["CAPYBARA_DRIVER"] + Capybara.default_driver = ENV["CAPYBARA_DRIVER"].to_sym + Capybara.javascript_driver = ENV["CAPYBARA_DRIVER"].to_sym +else + Capybara.default_driver = :selenium_chrome_customised_headless + Capybara.javascript_driver = :selenium_chrome_customised_headless +end +Capybara.enable_aria_label = true + +Capybara::Screenshot.register_driver(:selenium_chrome_customised_headless) do |driver, path| + driver.browser.save_screenshot(path) +end Capybara::Screenshot.register_filename_prefix_formatter(:rspec) do |example| "screenshot_#{example.description.tr(' ', '-').gsub(%r{^.*/spec/}, '')}" end -width = 1280 -height = 1280 -Capybara.current_session.driver.browser.manage.window.resize_to(width, height) Capybara.app_host = 'http://localhost' Capybara.server_port = 8081 +# TODO: Find a better home. +shared_examples 'is accessible' do + it "is accessible" do + expect(page).to be_axe_clean.skipping('color-contrast', 'heading-order', 'aria-required-children').according_to :wcag2a + end +end + include Warden::Test::Helpers # Requires supporting ruby files with custom matchers and macros, etc, in @@ -99,7 +126,13 @@ RSpec.configure do |config| config.include FactoryBot::Syntax::Methods # Prevent Poltergeist from fetching external URLs during feature tests - config.before(:each, js: true) do + config.before(:each, :js) do + + # TODO: Why are we setting this page size then straight afterwards, maximising? + width = 1280 + height = 1280 + Capybara.current_session.driver.browser.manage.window.resize_to(width, height) + if page.driver.browser.respond_to?(:url_blacklist) page.driver.browser.url_blacklist = [ 'gravatar.com', diff --git a/spec/requests/api/v1/harvest_request_spec.rb b/spec/requests/api/v1/harvests_request_spec.rb similarity index 100% rename from spec/requests/api/v1/harvest_request_spec.rb rename to spec/requests/api/v1/harvests_request_spec.rb diff --git a/spec/requests/api/v1/member_request_spec.rb b/spec/requests/api/v1/members_request_spec.rb similarity index 100% rename from spec/requests/api/v1/member_request_spec.rb rename to spec/requests/api/v1/members_request_spec.rb diff --git a/spec/requests/api/v1/seeds_request_spec.rb b/spec/requests/api/v1/seeds_request_spec.rb index 4106c0f77..d24b5bee1 100644 --- a/spec/requests/api/v1/seeds_request_spec.rb +++ b/spec/requests/api/v1/seeds_request_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'Photos', type: :request do +RSpec.describe 'Seeds', type: :request do subject { JSON.parse response.body } let(:headers) { { 'Accept' => 'application/vnd.api+json' } } diff --git a/spec/requests/plantings_spec.rb b/spec/requests/plantings_spec.rb index 90a4b5348..9276aaf03 100644 --- a/spec/requests/plantings_spec.rb +++ b/spec/requests/plantings_spec.rb @@ -10,4 +10,45 @@ describe "Plantings" do response.status.should be(200) end end + + context "with a member" do + before do + @member = create(:interesting_member) + @predictable_planting = create(:predictable_planting, owner: @member) + @seedling_planting = create(:seedling_planting, owner: @member) + @seed_planting = create(:seed_planting, owner: @member) + @finished_planting = create(:finished_planting, owner: @member) + @annual_planting = create(:annual_planting, owner: @member) + @perennial_planting = create(:perennial_planting, owner: @member) + + Planting.reindex + end + + describe "GET /members/x/plantings.ics" do + it "works!" do + get member_plantings_path(@member, format: "ics") + + calendar = Icalendar::Parser.new(response.body, true).parse.first + expect(calendar.description[0].to_s).to eq "Plantings by #{@member.login_name}" + events = calendar.events + expect(events.length).to eq 6 # There are 7, but finished plantings aren't included + + # TODO: Better date comparison + # Predicted finish should be used + expect(events[1].summary.to_s).to include @predictable_planting.crop.name + expect(events[1].dtstart.to_datetime.to_i).to be_within(1.second).of @predictable_planting.created_at.to_i + expect(events[1].dtend.to_date).to eq @predictable_planting.finish_predicted_at + + # Actual finish should be used + # expect(events[4].dtend.to_date).to be_within(1.second).of @finised_planting.finished_at + + # Otherwise, tomorrow should be used + expect(events[2].dtend.to_date).to eq 1.day.from_now.to_date + + # TBA: Perennial and annual crops predictions of 'next' harvest date don't really fit + + response.status.should be(200) + end + end + end end diff --git a/spec/routing/roles_routing_spec.rb b/spec/routing/admin/roles_controller_routing_spec.rb similarity index 100% rename from spec/routing/roles_routing_spec.rb rename to spec/routing/admin/roles_controller_routing_spec.rb diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_controller_routing_spec.rb similarity index 100% rename from spec/routing/admin_routing_spec.rb rename to spec/routing/admin_controller_routing_spec.rb diff --git a/spec/routing/authentications_routing_spec.rb b/spec/routing/authentications_controller_routing_spec.rb similarity index 100% rename from spec/routing/authentications_routing_spec.rb rename to spec/routing/authentications_controller_routing_spec.rb diff --git a/spec/routing/comments_routing_spec.rb b/spec/routing/comments_controller_routing_spec.rb similarity index 100% rename from spec/routing/comments_routing_spec.rb rename to spec/routing/comments_controller_routing_spec.rb diff --git a/spec/routing/crops_routing_spec.rb b/spec/routing/crops_controller_routing_spec.rb similarity index 100% rename from spec/routing/crops_routing_spec.rb rename to spec/routing/crops_controller_routing_spec.rb diff --git a/spec/routing/follows_routing_spec.rb b/spec/routing/follows_controller_routing_spec.rb similarity index 100% rename from spec/routing/follows_routing_spec.rb rename to spec/routing/follows_controller_routing_spec.rb diff --git a/spec/routing/forums_routing_spec.rb b/spec/routing/forums_controller_routing_spec.rb similarity index 100% rename from spec/routing/forums_routing_spec.rb rename to spec/routing/forums_controller_routing_spec.rb diff --git a/spec/routing/garden_types_routing_spec.rb b/spec/routing/garden_types_controller_routing_spec.rb similarity index 100% rename from spec/routing/garden_types_routing_spec.rb rename to spec/routing/garden_types_controller_routing_spec.rb diff --git a/spec/routing/gardens_routing_spec.rb b/spec/routing/gardens_controller_routing_spec.rb similarity index 100% rename from spec/routing/gardens_routing_spec.rb rename to spec/routing/gardens_controller_routing_spec.rb diff --git a/spec/routing/harvests_routing_spec.rb b/spec/routing/harvests_controller_routing_spec.rb similarity index 100% rename from spec/routing/harvests_routing_spec.rb rename to spec/routing/harvests_controller_routing_spec.rb diff --git a/spec/routing/member_routing_spec.rb b/spec/routing/members_controller_routing_spec.rb similarity index 100% rename from spec/routing/member_routing_spec.rb rename to spec/routing/members_controller_routing_spec.rb diff --git a/spec/routing/photos_routing_spec.rb b/spec/routing/photos_controller_routing_spec.rb similarity index 100% rename from spec/routing/photos_routing_spec.rb rename to spec/routing/photos_controller_routing_spec.rb diff --git a/spec/routing/plant_parts_routing_spec.rb b/spec/routing/plant_parts_controller_routing_spec.rb similarity index 100% rename from spec/routing/plant_parts_routing_spec.rb rename to spec/routing/plant_parts_controller_routing_spec.rb diff --git a/spec/routing/plantings_routing_spec.rb b/spec/routing/plantings_controller_routing_spec.rb similarity index 100% rename from spec/routing/plantings_routing_spec.rb rename to spec/routing/plantings_controller_routing_spec.rb diff --git a/spec/routing/updates_routing_spec.rb b/spec/routing/posts_controller_updates_routing_spec.rb similarity index 100% rename from spec/routing/updates_routing_spec.rb rename to spec/routing/posts_controller_updates_routing_spec.rb diff --git a/spec/routing/scientific_names_routing_spec.rb b/spec/routing/scientific_names_controller_routing_spec.rb similarity index 100% rename from spec/routing/scientific_names_routing_spec.rb rename to spec/routing/scientific_names_controller_routing_spec.rb diff --git a/spec/routing/seeds_routing_spec.rb b/spec/routing/seeds_controller_routing_spec.rb similarity index 100% rename from spec/routing/seeds_routing_spec.rb rename to spec/routing/seeds_controller_routing_spec.rb diff --git a/spec/services/gbif_service_spec.rb b/spec/services/gbif_service_spec.rb new file mode 100644 index 000000000..3384fcdb8 --- /dev/null +++ b/spec/services/gbif_service_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe GbifService, :vcr, type: :service do + let(:scientific_name_tomato) { create(:solanum_lycopersicum) } + let(:tomato) { scientific_name_tomato.crop } + + let(:gbif_service) { described_class.new } + + # TODO: Find places where we should just use dependency injection to insert the cropbot user. + before do + # don't use 'let' for this -- we need to actually create it, + # regardless of whether it's used. + @cropbot = FactoryBot.create(:cropbot) + end + + describe "#fetch" do + it "fetches a given key" do + result = gbif_service.fetch(2_930_137) + expect(result["key"]).to eq 2_930_137 + expect(result["family"]).to eq "Solanaceae" + end + end + + describe "#suggest" do + it "matches" do + results = gbif_service.suggest(scientific_name_tomato.name) + expect(results[0]["key"]).to eq 2_930_137 + expect(results[0]["family"]).to eq "Solanaceae" + end + end + + describe "#import!" + describe "#update_crop" do + it "resolves scientific names" do + gbif_service.update_crop(tomato) + + scientific_name_tomato.reload + expect(scientific_name_tomato.gbif_key).to eq 2_930_137 + end + + it "resolves common names" do + crop = create(:crop, name: "Habanero") + + gbif_service.update_crop(crop) + crop.reload + + expect(crop.scientific_names.first.name).to eq "Capsicum chinense" + end + + it "gets photos" do + scientific_name_tomato.update(gbif_key: "2930137") + + gbif_service.update_crop(tomato) + + tomato.reload + expect(tomato.photos.count).to eq 3 + + photo = tomato.photos.order(:id)[0] + expect(photo.fullsize_url).to eq "https://inaturalist-open-data.s3.amazonaws.com/photos/343874350/original.jpeg" + expect(photo.thumbnail_url).to eq "https://api.gbif.org/v1/image/cache/200x/occurrence/4507688130/media/7bc2c1b87c7110b785674bfc198d891c" + expect(photo.title).to eq "Photo by Ingeborg van Leeuwen via iNaturalist (Copyright Ingeborg van Leeuwen)" + expect(photo.license_name).to eq "CC BY-NC 4.0" + expect(photo.license_url).to eq "http://creativecommons.org/licenses/by-nc/4.0/" + expect(photo.link_url).to eq "https://www.inaturalist.org/photos/343874350" + expect(photo.source_id).to eq("4507688130") + expect(photo.source).to eq("gbif") + expect(photo.date_taken).to eq("2024-01-01T11:07:00.000+00:00") + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 02a2f3e02..d4f626953 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,6 +18,14 @@ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration require 'simplecov' require 'percy/capybara' +require 'vcr' + +VCR.configure do |c| + c.ignore_host "elasticsearch", "localhost" + c.cassette_library_dir = 'spec/cassettes' + c.hook_into :faraday + c.configure_rspec_metadata! +end SimpleCov.start @@ -53,7 +61,7 @@ RSpec.configure do |config| Searchkick.disable_callbacks end - config.around(:each, search: true) do |example| + config.around(:each, :search) do |example| Searchkick.callbacks(true) do index_everything example.run diff --git a/spec/support/feature_helpers.rb b/spec/support/feature_helpers.rb index e13837157..8040cdd3a 100644 --- a/spec/support/feature_helpers.rb +++ b/spec/support/feature_helpers.rb @@ -16,10 +16,12 @@ module FeatureHelpers let(:member) { FactoryBot.create(:member) } include_examples 'sign in' end + shared_context 'signed in crop wrangler' do let(:member) { FactoryBot.create(:crop_wrangling_member) } include_examples 'sign in' end + shared_context 'signed in admin' do let(:member) { FactoryBot.create(:admin_member) } include_examples 'sign in' diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb index c63bf5ff4..f3e6d536e 100644 --- a/spec/swagger_helper.rb +++ b/spec/swagger_helper.rb @@ -6,11 +6,11 @@ RSpec.configure do |config| # Specify a root folder where Swagger JSON files are generated # NOTE: If you're using the rswag-api to serve API descriptions, you'll need # to ensure that it's configured to serve Swagger from the same folder - config.swagger_root = Rails.root.join('swagger').to_s + config.openapi_root = Rails.root.join('swagger').to_s # Define one or more Swagger documents and provide global metadata for each one # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will - # be generated at the provided relative path under swagger_root + # be generated at the provided relative path under openapi_root # By default, the operations defined in spec files are added to the first # document below. You can override this behavior by adding a swagger_doc tag to the # the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json' diff --git a/spec/views/admin/newsletter_spec.rb b/spec/views/admin/newsletter_spec.rb deleted file mode 100644 index 464f2d6c5..000000000 --- a/spec/views/admin/newsletter_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe 'admin/newsletter.html.haml', type: "view" do - before do - @member = FactoryBot.create(:admin_member) - sign_in @member - controller.stub(:current_user) { @member } - @subscriber = FactoryBot.create(:newsletter_recipient_member) - assign(:members, [@subscriber]) - render - end - - it "lists newsletter subscribers by email" do - expect(rendered).to have_content @subscriber.email - end -end diff --git a/spec/views/admin/roles/edit.html.haml_spec.rb b/spec/views/admin/roles/edit.html.haml_spec.rb deleted file mode 100644 index 65cf5e3db..000000000 --- a/spec/views/admin/roles/edit.html.haml_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe "admin/roles/edit" do - before do - @role = assign(:role, stub_model(Role, - name: "MyString", - description: "MyText")) - end - - it "renders the edit role form" do - render - - assert_select "form", action: admin_roles_path(@role), method: "post" do - assert_select "input#role_name", name: "role[name]" - assert_select "textarea#role_description", name: "role[description]" - end - end -end diff --git a/spec/views/admin/roles/index.html.haml_spec.rb b/spec/views/admin/roles/index.html.haml_spec.rb deleted file mode 100644 index 267cf78ba..000000000 --- a/spec/views/admin/roles/index.html.haml_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe "admin/roles/index" do - before do - controller.stub(:current_user) { nil } - assign(:roles, [ - stub_model(Role, - name: "Name", - description: "MyText"), - stub_model(Role, - name: "Name", - description: "MyText") - ]) - end - - it "renders a list of roles" do - render - # Run the generator again with the --webrat flag if you want to use webrat matchers - assert_select "tr>td", text: "Name".to_s, count: 2 - assert_select "tr>td", text: "MyText".to_s, count: 2 - end -end diff --git a/spec/views/admin/roles/new.html.haml_spec.rb b/spec/views/admin/roles/new.html.haml_spec.rb deleted file mode 100644 index 3372f836b..000000000 --- a/spec/views/admin/roles/new.html.haml_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe "admin/roles/new" do - before do - assign(:role, stub_model(Role, - name: "MyString", - description: "MyText").as_new_record) - end - - it "renders new role form" do - render - - # Run the generator again with the --webrat flag if you want to use webrat matchers - assert_select "form", action: admin_roles_path, method: "post" do - assert_select "input#role_name", name: "role[name]" - assert_select "textarea#role_description", name: "role[description]" - end - end -end diff --git a/spec/views/comments/index.rss.haml_spec.rb b/spec/views/comments/index.rss.haml_spec.rb index 38b5a2386..ff7009ffe 100644 --- a/spec/views/comments/index.rss.haml_spec.rb +++ b/spec/views/comments/index.rss.haml_spec.rb @@ -24,7 +24,7 @@ describe 'comments/index.rss.haml' do it 'escapes html for link to post' do # it's then unescaped by 'render' so we don't actually look for < - rendered.should have_content 'td", text: "Zea mays".to_s - assert_select "tr>td", text: "Solanum lycopersicum".to_s - end - - it "doesn't show edit/destroy links" do - render - rendered.should_not have_content "Edit" - rendered.should_not have_content "Delete" - end - - context "logged in and crop wrangler" do - before do - @member = FactoryBot.create(:crop_wrangling_member) - sign_in @member - controller.stub(:current_user) { @member } - end - - it "shows edit/destroy links" do - render - rendered.should have_content "Edit" - rendered.should have_content "Delete" - end - end -end diff --git a/spec/views/scientific_names/new.html.haml_spec.rb b/spec/views/scientific_names/new.html.haml_spec.rb deleted file mode 100644 index 39dbedd88..000000000 --- a/spec/views/scientific_names/new.html.haml_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe "scientific_names/new" do - before do - assign(:scientific_name, FactoryBot.create(:zea_mays)) - end - - context "logged in" do - before do - @member = FactoryBot.create(:member) - sign_in @member - controller.stub(:current_user) { @member } - render - end - - it "renders new scientific_name form" do - render - # Run the generator again with the --webrat flag if you want to use webrat matchers - assert_select "form", action: scientific_names_path, method: "post" do - assert_select "input#scientific_name_name", name: "scientific_name[scientific_name]" - assert_select "select#scientific_name_crop_id", name: "scientific_name[crop_id]" - end - end - end -end diff --git a/spec/views/scientific_names/show.html.haml_spec.rb b/spec/views/scientific_names/show.html.haml_spec.rb deleted file mode 100644 index 1661ae8d2..000000000 --- a/spec/views/scientific_names/show.html.haml_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe "scientific_names/show" do - before do - controller.stub(:current_user) { nil } - @scientific_name = assign(:scientific_name, - FactoryBot.create(:zea_mays)) - end - - it "renders attributes in

    " do - render - # Run the generator again with the --webrat flag if you want to use webrat matchers - rendered.should match(/Zea mays/) - end - - context 'signed in' do - before do - @wrangler = FactoryBot.create(:crop_wrangling_member) - sign_in @wrangler - controller.stub(:current_user) { @wrangler } - render - end - - it 'has an edit button' do - rendered.should have_content 'Edit' - end - end -end