Merge pull request #3546 from Growstuff/dev

January 2024 Release
This commit is contained in:
Daniel O'Connor
2024-02-03 18:24:57 +10:30
committed by GitHub
176 changed files with 1843 additions and 711 deletions

View File

@@ -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 <your-package-list-here>
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 <your-gem-names-here>

View File

@@ -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

View File

@@ -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 }}

View File

@@ -7,7 +7,7 @@ AllCops:
Exclude:
- 'db/schema.rb'
- 'vendor/**/*'
TargetRailsVersion: 6.0
TargetRailsVersion: 7.0
Rails:
Enabled: true

View File

@@ -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:

View File

@@ -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)

26
Gemfile
View File

@@ -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

View File

@@ -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

View File

@@ -5,7 +5,7 @@ jQuery ->
$(".remove-altname-row").css("display", "inline-block")
-$ ->
sci_template = "<div id='sci_template[INDEX]' class='template col-md-12'><div class='col-md-2'><label>Scientific name INDEX:</label></div><div class='col-md-8'><input name='sci_name[INDEX]' class='form-control', id='sci_name[INDEX]')'></input><span class='help-block'>Scientific name of crop.</span></div><div class='col-md-2'></div></div>"
sci_template = "<div id='sci_template[INDEX]' class='template col-md-12'><div class='col-md-2'><label>Scientific name INDEX:</label></div><div class='col-md-8'><input name='sci_name[INDEX]' class='scientific-name-auto-suggest form-control' id='sci_name[INDEX]' data-source-url='/scientific_names/gbif_suggest')'></input><span class='help-block'>Scientific name of crop</span><input type='text' id='sci_gbif_key[INDEX]' class=''></div><div class='col-md-2'></div></div>"
sci_index = $('#scientific_names .template').length + 1
@@ -21,7 +21,7 @@ jQuery ->
element = document.getElementById(tmp)
element.remove()
alt_template = "<div id='alt_template[INDEX]' class='template col-md-12'><div class='col-md-2'><label>Alternate name INDEX:</label></div><div class='col-md-8'><input name='alt_name[INDEX]' class='form-control', id='alt_name[INDEX]')'></input><span class='help-block'>Alternate name of crop.</span></div><div class='col-md-2'></div></div>"
alt_template = "<div id='alt_template[INDEX]' class='template col-md-12'><div class='col-md-2'><label>Alternate name INDEX:</label></div><div class='col-md-8'><input name='alt_name[INDEX]' class='form-control' id='alt_name[INDEX]')'></input><span class='help-block'>Alternate name of crop.</span></div><div class='col-md-2'></div></div>"
alt_index = $('#alternate_names .template').length + 1

View File

@@ -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 ) ->
$( '<li class="list-group-item"></li>' )
.data( 'item.autocomplete', item )
.append( "<a>#{item.canonicalName} (#{item.scientificName}) - #{item.rank}</a>" )
.appendTo( ul )

View File

@@ -19,7 +19,7 @@
padding-bottom: 0;
}
h5.crop-sci-name {
.crop-sci-name {
background-color: $beige;
color: $black;
font-size: 0.7em;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -4,6 +4,7 @@ class Crop < ApplicationRecord
extend FriendlyId
include PhotoCapable
include OpenFarmData
include GbifData
include SearchCrops
friendly_id :name, use: %i(slugged finders)

View File

@@ -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|

View File

@@ -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}"

View File

@@ -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

View File

@@ -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

View File

@@ -4,3 +4,4 @@
- @members.each do |m|
= m.email
%br/
= will_paginate @members

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -7,7 +7,7 @@
%item
%title Comment by #{comment.author.login_name} on #{comment.post.subject}
%description
:escaped
:escaped_markdown
<p>
Comment on
#{ link_to comment.post.subject, post_url(comment.post) }

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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',

View File

@@ -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}"

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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

View File

@@ -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"

View File

@@ -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:"

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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'}

View File

@@ -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'))

View File

@@ -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}"

View File

@@ -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']}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
/

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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'

View File

@@ -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

View File

@@ -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 <a href="https://inaturalist.org/" target="_blank">iNaturalist</a> or <a href="https://identify.plantnet.org/" target="_blank">Pl@ntNet</a> via the app.
- if @please_reconnect_flickr
%h2.alert Please reconnect your flickr account

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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 %>

View File

@@ -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?

View File

@@ -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'

View File

@@ -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

View File

@@ -8,7 +8,7 @@
%title Comment by #{comment.author.login_name} on #{comment.created_at}
%description
:escaped
:escaped_markdown
<p>
Comment on
#{ link_to @post.subject, post_url(@post) }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.'

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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|

View File

@@ -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"

View File

@@ -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

View File

@@ -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

10
lib/tasks/gbif.rake Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More