mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-05-30 03:36:23 -04:00
Compare commits
30 Commits
feature/we
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d2bc77731 | ||
|
|
a32d7d11c9 | ||
|
|
5c0746f136 | ||
|
|
38736211ec | ||
|
|
2dc5dbac7d | ||
|
|
ee6d5cd84a | ||
|
|
f1524c2b0c | ||
|
|
a6cb1cbb36 | ||
|
|
c0f6720a1e | ||
|
|
04ea628f00 | ||
|
|
cabac926cc | ||
|
|
0d375f6146 | ||
|
|
be9e5ed6bf | ||
|
|
c8ea225b10 | ||
|
|
a8b7c73111 | ||
|
|
61810dbee3 | ||
|
|
1467ec9364 | ||
|
|
37452a5513 | ||
|
|
e70297a83e | ||
|
|
d7a50f86b5 | ||
|
|
ca7f56683c | ||
|
|
0c00b866da | ||
|
|
f50da4e0e0 | ||
|
|
31285b2bde | ||
|
|
ec5873fc88 | ||
|
|
555d5ddf15 | ||
|
|
5ada7e7f77 | ||
|
|
4659ac5464 | ||
|
|
3d63d12908 | ||
|
|
035210197f |
4
Gemfile
4
Gemfile
@@ -130,10 +130,6 @@ gem 'rack-cors'
|
|||||||
|
|
||||||
gem 'icalendar'
|
gem 'icalendar'
|
||||||
|
|
||||||
# for web push notifications
|
|
||||||
gem 'web-push'
|
|
||||||
gem 'serviceworker-rails'
|
|
||||||
|
|
||||||
# for signups as requested by email service
|
# for signups as requested by email service
|
||||||
gem 'recaptcha'
|
gem 'recaptcha'
|
||||||
|
|
||||||
|
|||||||
56
Gemfile.lock
56
Gemfile.lock
@@ -122,8 +122,8 @@ GEM
|
|||||||
autoprefixer-rails (10.4.16.0)
|
autoprefixer-rails (10.4.16.0)
|
||||||
execjs (~> 2)
|
execjs (~> 2)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1240.0)
|
aws-partitions (1.1252.0)
|
||||||
aws-sdk-core (3.245.0)
|
aws-sdk-core (3.248.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
@@ -131,11 +131,11 @@ GEM
|
|||||||
bigdecimal
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
logger
|
logger
|
||||||
aws-sdk-kms (1.123.0)
|
aws-sdk-kms (1.128.0)
|
||||||
aws-sdk-core (~> 3, >= 3.244.0)
|
aws-sdk-core (~> 3, >= 3.248.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.220.0)
|
aws-sdk-s3 (1.224.0)
|
||||||
aws-sdk-core (~> 3, >= 3.244.0)
|
aws-sdk-core (~> 3, >= 3.248.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.12.1)
|
aws-sigv4 (1.12.1)
|
||||||
@@ -177,7 +177,7 @@ GEM
|
|||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
activemodel (>= 6.1)
|
activemodel (>= 6.1)
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
bullet (8.1.1)
|
bullet (8.1.2)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
byebug (13.0.0)
|
byebug (13.0.0)
|
||||||
@@ -236,7 +236,7 @@ GEM
|
|||||||
csv_shaper (1.4.0)
|
csv_shaper (1.4.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
csv
|
csv
|
||||||
dalli (5.0.2)
|
dalli (5.0.5)
|
||||||
logger
|
logger
|
||||||
database_cleaner (2.1.0)
|
database_cleaner (2.1.0)
|
||||||
database_cleaner-active_record (>= 2, < 3)
|
database_cleaner-active_record (>= 2, < 3)
|
||||||
@@ -247,7 +247,7 @@ GEM
|
|||||||
date (3.5.1)
|
date (3.5.1)
|
||||||
descendants_tracker (0.0.4)
|
descendants_tracker (0.0.4)
|
||||||
thread_safe (~> 0.3, >= 0.3.1)
|
thread_safe (~> 0.3, >= 0.3.1)
|
||||||
devise (5.0.3)
|
devise (5.0.4)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 7.0)
|
railties (>= 7.0)
|
||||||
@@ -286,7 +286,7 @@ GEM
|
|||||||
railties (>= 6.1.0)
|
railties (>= 6.1.0)
|
||||||
faker (3.8.0)
|
faker (3.8.0)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (2.14.1)
|
faraday (2.14.2)
|
||||||
faraday-net_http (>= 2.0, < 3.5)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
json
|
json
|
||||||
logger
|
logger
|
||||||
@@ -297,7 +297,7 @@ GEM
|
|||||||
flickraw (0.9.10)
|
flickraw (0.9.10)
|
||||||
font-awesome-sass (5.15.1)
|
font-awesome-sass (5.15.1)
|
||||||
sassc (>= 1.11)
|
sassc (>= 1.11)
|
||||||
friendly_id (5.6.0)
|
friendly_id (5.7.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
gbifrb (0.2.0)
|
gbifrb (0.2.0)
|
||||||
geocoder (1.8.6)
|
geocoder (1.8.6)
|
||||||
@@ -355,7 +355,7 @@ GEM
|
|||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
i18n_data (1.1.0)
|
i18n_data (1.1.0)
|
||||||
simple_po_parser (~> 1.1)
|
simple_po_parser (~> 1.1)
|
||||||
icalendar (2.12.2)
|
icalendar (2.12.3)
|
||||||
base64
|
base64
|
||||||
ice_cube (~> 0.16)
|
ice_cube (~> 0.16)
|
||||||
logger
|
logger
|
||||||
@@ -366,7 +366,7 @@ GEM
|
|||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
io-console (0.8.2)
|
io-console (0.8.2)
|
||||||
irb (1.17.0)
|
irb (1.18.0)
|
||||||
pp (>= 0.6.0)
|
pp (>= 0.6.0)
|
||||||
prism (>= 1.3.0)
|
prism (>= 1.3.0)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
@@ -376,7 +376,7 @@ GEM
|
|||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
json (2.19.4)
|
json (2.19.7)
|
||||||
json-schema (6.2.0)
|
json-schema (6.2.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
bigdecimal (>= 3.1, < 5)
|
bigdecimal (>= 3.1, < 5)
|
||||||
@@ -385,8 +385,6 @@ GEM
|
|||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
railties (>= 4.1)
|
railties (>= 4.1)
|
||||||
jsonapi-swagger (0.8.1)
|
jsonapi-swagger (0.8.1)
|
||||||
jwt (3.1.2)
|
|
||||||
base64
|
|
||||||
kgio (2.11.4)
|
kgio (2.11.4)
|
||||||
kramdown (2.4.0)
|
kramdown (2.4.0)
|
||||||
rexml
|
rexml
|
||||||
@@ -455,13 +453,13 @@ GEM
|
|||||||
net-protocol
|
net-protocol
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
nio4r (2.7.5)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.19.2)
|
nokogiri (1.19.3)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.2-x86_64-linux-gnu)
|
nokogiri (1.19.3-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (0.5.6)
|
oauth (0.5.6)
|
||||||
oj (3.17.0)
|
oj (3.17.1)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
omniauth (1.9.2)
|
omniauth (1.9.2)
|
||||||
@@ -474,7 +472,6 @@ GEM
|
|||||||
oauth
|
oauth
|
||||||
omniauth (~> 1.0)
|
omniauth (~> 1.0)
|
||||||
open-uri (0.1.0)
|
open-uri (0.1.0)
|
||||||
openssl (3.3.0)
|
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostruct (0.6.3)
|
ostruct (0.6.3)
|
||||||
paper_trail (17.0.0)
|
paper_trail (17.0.0)
|
||||||
@@ -501,7 +498,7 @@ GEM
|
|||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (7.0.5)
|
public_suffix (7.0.5)
|
||||||
puma (8.0.1)
|
puma (8.0.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
query_diet (0.7.3)
|
query_diet (0.7.3)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
@@ -633,7 +630,7 @@ GEM
|
|||||||
rswag-ui (2.17.0)
|
rswag-ui (2.17.0)
|
||||||
actionpack (>= 5.2, < 8.2)
|
actionpack (>= 5.2, < 8.2)
|
||||||
railties (>= 5.2, < 8.2)
|
railties (>= 5.2, < 8.2)
|
||||||
rubocop (1.86.1)
|
rubocop (1.86.2)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
@@ -653,7 +650,7 @@ GEM
|
|||||||
rubocop-factory_bot (2.28.0)
|
rubocop-factory_bot (2.28.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (~> 1.72, >= 1.72.1)
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
rubocop-rails (2.34.3)
|
rubocop-rails (2.35.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
@@ -673,7 +670,7 @@ GEM
|
|||||||
ruby-units (4.1.0)
|
ruby-units (4.1.0)
|
||||||
ruby-vips (2.2.1)
|
ruby-vips (2.2.1)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
rubyzip (3.2.2)
|
rubyzip (3.3.0)
|
||||||
sass (3.7.4)
|
sass (3.7.4)
|
||||||
sass-listen (~> 4.0.0)
|
sass-listen (~> 4.0.0)
|
||||||
sass-listen (4.0.0)
|
sass-listen (4.0.0)
|
||||||
@@ -693,14 +690,12 @@ GEM
|
|||||||
activemodel (>= 6.1)
|
activemodel (>= 6.1)
|
||||||
hashie
|
hashie
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.43.0)
|
selenium-webdriver (4.44.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
logger (~> 1.4)
|
logger (~> 1.4)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
rubyzip (>= 1.2.2, < 4.0)
|
rubyzip (>= 1.2.2, < 4.0)
|
||||||
websocket (~> 1.0)
|
websocket (~> 1.0)
|
||||||
serviceworker-rails (0.6.0)
|
|
||||||
railties (>= 3.1)
|
|
||||||
sidekiq (7.3.10)
|
sidekiq (7.3.10)
|
||||||
base64
|
base64
|
||||||
connection_pool (>= 2.3.0, < 3)
|
connection_pool (>= 2.3.0, < 3)
|
||||||
@@ -730,7 +725,7 @@ GEM
|
|||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tilt (2.7.0)
|
tilt (2.7.0)
|
||||||
timecop (0.9.11)
|
timecop (0.9.11)
|
||||||
timeout (0.5.0)
|
timeout (0.6.1)
|
||||||
tsort (0.2.0)
|
tsort (0.2.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
@@ -753,9 +748,6 @@ GEM
|
|||||||
descendants_tracker (~> 0.0, >= 0.0.3)
|
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-push (3.0.2)
|
|
||||||
jwt (~> 3.0)
|
|
||||||
openssl (~> 3.0)
|
|
||||||
webrat (0.7.3)
|
webrat (0.7.3)
|
||||||
nokogiri (>= 1.2.0)
|
nokogiri (>= 1.2.0)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
@@ -880,7 +872,6 @@ DEPENDENCIES
|
|||||||
scout_apm
|
scout_apm
|
||||||
searchkick
|
searchkick
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
serviceworker-rails
|
|
||||||
sidekiq
|
sidekiq
|
||||||
sitemap_generator
|
sitemap_generator
|
||||||
sprockets (< 4)
|
sprockets (< 4)
|
||||||
@@ -889,7 +880,6 @@ DEPENDENCIES
|
|||||||
unicorn
|
unicorn
|
||||||
validate_url
|
validate_url
|
||||||
vcr
|
vcr
|
||||||
web-push
|
|
||||||
webrat
|
webrat
|
||||||
will_paginate
|
will_paginate
|
||||||
will_paginate-bootstrap-style
|
will_paginate-bootstrap-style
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// = link_tree ../images
|
// = link_tree ../images
|
||||||
// = link serviceworker.js
|
|
||||||
// = link_directory ../javascripts .js
|
// = link_directory ../javascripts .js
|
||||||
// = link_directory ../stylesheets .css
|
// = link_directory ../stylesheets .css
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
//
|
|
||||||
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
|
||||||
// listed below.
|
|
||||||
//
|
|
||||||
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
|
|
||||||
// vendor/assets/javascripts directory can be referenced here using a relative path.
|
|
||||||
//
|
|
||||||
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
|
||||||
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
|
||||||
//
|
|
||||||
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
|
||||||
// about supported directives.
|
|
||||||
//
|
|
||||||
//= require rails-ujs
|
|
||||||
//= require activestorage
|
|
||||||
//= require_tree .
|
|
||||||
|
|
||||||
function urlBase64ToUint8Array(base64String) {
|
|
||||||
const padding = '='.repeat((4 - base64String.length % 4) % 4);
|
|
||||||
const base64 = (base64String + padding)
|
|
||||||
.replace(/-/g, '+')
|
|
||||||
.replace(/_/g, '/');
|
|
||||||
|
|
||||||
const rawData = window.atob(base64);
|
|
||||||
const outputArray = new Uint8Array(rawData.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < rawData.length; ++i) {
|
|
||||||
outputArray[i] = rawData.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return outputArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const pushButton = document.getElementById('enable-push-notifications');
|
|
||||||
if (pushButton) {
|
|
||||||
pushButton.addEventListener('click', () => {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
const vapidPublicKey = document.querySelector('meta[name="vapid-public-key"]').content;
|
|
||||||
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
|
|
||||||
|
|
||||||
registration.pushManager.subscribe({
|
|
||||||
userVisibleOnly: true,
|
|
||||||
applicationServerKey: convertedVapidKey
|
|
||||||
}).then(subscription => {
|
|
||||||
fetch('/push_subscriptions', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ subscription: subscription.toJSON() })
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
self.addEventListener('push', function(event) {
|
|
||||||
const data = event.data.json();
|
|
||||||
const title = data.title || 'Growstuff';
|
|
||||||
const options = {
|
|
||||||
body: data.body,
|
|
||||||
icon: '/assets/growstuff-apple-touch-icon-precomposed.png',
|
|
||||||
badge: '/assets/growstuff-apple-touch-icon-precomposed.png'
|
|
||||||
};
|
|
||||||
|
|
||||||
event.waitUntil(
|
|
||||||
self.registration.showNotification(title, options)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -3,6 +3,36 @@
|
|||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class CropsController < BaseController
|
class CropsController < BaseController
|
||||||
|
def search
|
||||||
|
term = params[:term]
|
||||||
|
page = params.dig(:page, :number) || 1
|
||||||
|
per_page = params.dig(:page, :size) || Crop.per_page
|
||||||
|
|
||||||
|
search_results = CropSearchService.search(
|
||||||
|
term,
|
||||||
|
page: page,
|
||||||
|
per_page: per_page,
|
||||||
|
load: true
|
||||||
|
)
|
||||||
|
|
||||||
|
resources = search_results.map do |crop|
|
||||||
|
Api::V1::CropResource.new(crop, context)
|
||||||
|
end
|
||||||
|
|
||||||
|
serializer = JSONAPI::ResourceSerializer.new(Api::V1::CropResource)
|
||||||
|
|
||||||
|
data = resources.map do |resource|
|
||||||
|
serializer.object_hash(resource, {})
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
data: data,
|
||||||
|
meta: {
|
||||||
|
record_count: search_results.total_count,
|
||||||
|
page_count: search_results.total_pages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -57,12 +57,23 @@ class GardensController < DataController
|
|||||||
redirect_to(member_gardens_path(@garden.owner))
|
redirect_to(member_gardens_path(@garden.owner))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch_wikidata
|
||||||
|
if @garden.populate_wikidata_info
|
||||||
|
@garden.save
|
||||||
|
flash[:notice] = "Wikidata information updated."
|
||||||
|
else
|
||||||
|
flash[:alert] = "Could not find Wikidata information for this location."
|
||||||
|
end
|
||||||
|
redirect_to @garden
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def garden_params
|
def garden_params
|
||||||
params.require(:garden).permit(
|
params.require(:garden).permit(
|
||||||
:name, :slug, :description, :active,
|
:name, :slug, :description, :active,
|
||||||
:location, :latitude, :longitude, :area, :area_unit, :garden_type_id
|
:location, :latitude, :longitude, :area, :area_unit, :garden_type_id,
|
||||||
|
:location_wikidata_id, :lowest_temp_c, :highest_temp_c
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ class HarvestsController < DataController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@harvest = Harvest.new(harvested_at: Time.zone.today)
|
@harvest = Harvest.new(new_harvest_params.merge(harvested_at: Time.zone.today))
|
||||||
@planting = Planting.find_by(slug: params[:planting_slug]) if params[:planting_slug]
|
@planting = @harvest.planting
|
||||||
@crop = Crop.find_by(id: params[:crop_id])
|
@crop = @harvest.crop
|
||||||
respond_with(@harvest)
|
respond_with(@harvest)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class HarvestsController < DataController
|
|||||||
def create
|
def create
|
||||||
@harvest.crop_id = @harvest.planting.crop_id if @harvest.planting_id
|
@harvest.crop_id = @harvest.planting.crop_id if @harvest.planting_id
|
||||||
@harvest.harvested_at = Time.zone.now if @harvest.harvested_at.blank?
|
@harvest.harvested_at = Time.zone.now if @harvest.harvested_at.blank?
|
||||||
@harvest.save
|
update_planting_rating if @harvest.save
|
||||||
if params[:return] == 'planting'
|
if params[:return] == 'planting'
|
||||||
respond_with(@harvest, location: @harvest.planting)
|
respond_with(@harvest, location: @harvest.planting)
|
||||||
else
|
else
|
||||||
@@ -61,7 +61,7 @@ class HarvestsController < DataController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@harvest.update(harvest_params)
|
update_planting_rating if @harvest.update(harvest_params)
|
||||||
respond_with(@harvest)
|
respond_with(@harvest)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -76,7 +76,17 @@ class HarvestsController < DataController
|
|||||||
params.require(:harvest)
|
params.require(:harvest)
|
||||||
.permit(:planting_id, :crop_id, :harvested_at, :description,
|
.permit(:planting_id, :crop_id, :harvested_at, :description,
|
||||||
:quantity, :unit, :weight_quantity, :weight_unit,
|
:quantity, :unit, :weight_quantity, :weight_unit,
|
||||||
:plant_part_id, :slug, :si_weight)
|
:plant_part_id, :slug, :si_weight, :overall_rating)
|
||||||
|
.merge(owner_id: current_member.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def new_harvest_params
|
||||||
|
return {} unless params[:harvest]
|
||||||
|
|
||||||
|
params.require(:harvest)
|
||||||
|
.permit(:planting_id, :crop_id, :harvested_at, :description,
|
||||||
|
:quantity, :unit, :weight_quantity, :weight_unit,
|
||||||
|
:plant_part_id, :slug, :si_weight, :overall_rating)
|
||||||
.merge(owner_id: current_member.id)
|
.merge(owner_id: current_member.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -103,4 +113,10 @@ class HarvestsController < DataController
|
|||||||
@harvest.planting.update_harvest_days!
|
@harvest.planting.update_harvest_days!
|
||||||
@harvest.crop.update_harvest_medians
|
@harvest.crop.update_harvest_medians
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_planting_rating
|
||||||
|
return if @harvest.planting.nil? || params[:harvest][:overall_rating].blank?
|
||||||
|
|
||||||
|
@harvest.planting.update(overall_rating: params[:harvest][:overall_rating])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class PushSubscriptionsController < ApplicationController
|
|
||||||
before_action :authenticate_member!
|
|
||||||
|
|
||||||
def create
|
|
||||||
subscription = current_member.push_subscriptions.find_or_initialize_by(endpoint: params[:subscription][:endpoint])
|
|
||||||
subscription.update(
|
|
||||||
p256dh: params[:subscription][:keys][:p256dh],
|
|
||||||
auth: params[:subscription][:keys][:auth]
|
|
||||||
)
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
|
|
||||||
def destroy
|
|
||||||
subscription = current_member.push_subscriptions.find_by(endpoint: params[:endpoint])
|
|
||||||
subscription&.destroy
|
|
||||||
head :ok
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class PushNotificationJob < ApplicationJob
|
|
||||||
queue_as :default
|
|
||||||
|
|
||||||
def perform(*args)
|
|
||||||
Member.where.not(timezone: nil).pluck(:timezone).uniq.each do |timezone|
|
|
||||||
Time.use_zone(timezone) do
|
|
||||||
if Time.zone.now.hour == 8
|
|
||||||
Member.where(timezone: timezone).each do |member|
|
|
||||||
send_planting_notifications(member)
|
|
||||||
send_activity_notifications(member)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def send_planting_notifications(member)
|
|
||||||
member.plantings.active.annual.each do |planting|
|
|
||||||
if planting.finish_is_predicatable? && (planting.late? || planting.super_late?)
|
|
||||||
PushNotificationService.new(member, "Your #{planting.crop_name} planting is ready to be marked as finished.").send
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def send_activity_notifications(member)
|
|
||||||
due_activities = member.activities.where(due_date: Date.today, finished: false)
|
|
||||||
due_activities.each do |activity|
|
|
||||||
PushNotificationService.new(member, "Activity due: #{activity.name}").send
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -123,6 +123,7 @@ class Ability
|
|||||||
can :create, GardenCollaborator, garden: { owner_id: member.id }
|
can :create, GardenCollaborator, garden: { owner_id: member.id }
|
||||||
can :update, GardenCollaborator, garden: { owner_id: member.id }
|
can :update, GardenCollaborator, garden: { owner_id: member.id }
|
||||||
can :destroy, GardenCollaborator, garden: { owner_id: member.id }
|
can :destroy, GardenCollaborator, garden: { owner_id: member.id }
|
||||||
|
can :destroy, GardenCollaborator, member_id: member.id
|
||||||
|
|
||||||
can :create, Activity
|
can :create, Activity
|
||||||
can :update, Activity, owner_id: member.id
|
can :update, Activity, owner_id: member.id
|
||||||
|
|||||||
@@ -165,9 +165,11 @@ class Crop < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def all_companions
|
def all_companions
|
||||||
return companions unless parent
|
@all_companions ||= if parent
|
||||||
|
(companions + parent.all_companions).uniq
|
||||||
(companions + parent.all_companions).uniq
|
else
|
||||||
|
companions
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
before_destroy :destroy_reverse_companionships
|
before_destroy :destroy_reverse_companionships
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class Garden < ApplicationRecord
|
|||||||
after_validation :cleanup_area
|
after_validation :cleanup_area
|
||||||
after_validation :geocode
|
after_validation :geocode
|
||||||
after_validation :empty_unwanted_geocodes
|
after_validation :empty_unwanted_geocodes
|
||||||
|
after_validation :populate_wikidata_info, if: :will_save_change_to_location?
|
||||||
after_save :mark_inactive_garden_plantings_as_finished
|
after_save :mark_inactive_garden_plantings_as_finished
|
||||||
|
|
||||||
scope :active, -> { where(active: true) }
|
scope :active, -> { where(active: true) }
|
||||||
@@ -92,6 +93,19 @@ class Garden < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def populate_wikidata_info
|
||||||
|
return false if location.blank?
|
||||||
|
|
||||||
|
wd_id = WikidataService.find_wikidata_id(location)
|
||||||
|
return false if wd_id.blank?
|
||||||
|
|
||||||
|
self.location_wikidata_id = wd_id
|
||||||
|
temps = WikidataService.fetch_temps(wd_id)
|
||||||
|
self.highest_temp_c = temps[:highest_temp_c]
|
||||||
|
self.lowest_temp_c = temps[:lowest_temp_c]
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def strip_blanks
|
def strip_blanks
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ class Harvest < ApplicationRecord
|
|||||||
include SearchHarvests
|
include SearchHarvests
|
||||||
include Likeable
|
include Likeable
|
||||||
|
|
||||||
|
attr_accessor :overall_rating
|
||||||
|
|
||||||
friendly_id :harvest_slug, use: %i(slugged finders)
|
friendly_id :harvest_slug, use: %i(slugged finders)
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class PushSubscription < ApplicationRecord
|
|
||||||
belongs_to :member
|
|
||||||
end
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class CropSearchService
|
class CropSearchService
|
||||||
# Crop.search(string)
|
# Crop.search(string)
|
||||||
def self.search(query, page: 1, per_page: 12, current_member: nil)
|
def self.search(query, page: 1, per_page: 12, current_member: nil, **options)
|
||||||
search_params = {
|
search_params = {
|
||||||
page:,
|
page:,
|
||||||
per_page:,
|
per_page:,
|
||||||
@@ -12,7 +12,7 @@ class CropSearchService
|
|||||||
includes: %i(scientific_names alternate_names),
|
includes: %i(scientific_names alternate_names),
|
||||||
misspellings: { edit_distance: 2 },
|
misspellings: { edit_distance: 2 },
|
||||||
load: false
|
load: false
|
||||||
}
|
}.merge(options)
|
||||||
# prioritise crops the member has planted
|
# prioritise crops the member has planted
|
||||||
search_params[:boost_where] = { planters_ids: current_member.id } if current_member
|
search_params[:boost_where] = { planters_ids: current_member.id } if current_member
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class PushNotificationService
|
|
||||||
def initialize(member, message)
|
|
||||||
@member = member
|
|
||||||
@message = message
|
|
||||||
end
|
|
||||||
|
|
||||||
def send
|
|
||||||
@member.push_subscriptions.each do |subscription|
|
|
||||||
begin
|
|
||||||
WebPush.payload_send(
|
|
||||||
message: JSON.generate(title: 'Growstuff', body: @message),
|
|
||||||
endpoint: subscription.endpoint,
|
|
||||||
p256dh: subscription.p256dh,
|
|
||||||
auth: subscription.auth,
|
|
||||||
vapid: {
|
|
||||||
subject: "mailto:#{ENV.fetch('GROWSTUFF_EMAIL', 'noreply@growstuff.org')}",
|
|
||||||
public_key: ENV['GROWSTUFF_VAPID_PUBLIC_KEY'],
|
|
||||||
private_key: ENV['GROWSTUFF_VAPID_PRIVATE_KEY']
|
|
||||||
}
|
|
||||||
)
|
|
||||||
rescue WebPush::InvalidSubscription => e
|
|
||||||
# A subscription can become invalid if the user revokes the permission.
|
|
||||||
# In this case, we should delete the subscription.
|
|
||||||
subscription.destroy
|
|
||||||
Rails.logger.info "Subscription deleted because it was invalid: #{e.message}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
74
app/services/wikidata_service.rb
Normal file
74
app/services/wikidata_service.rb
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'net/http'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
|
class WikidataService
|
||||||
|
CELSIUS_UNIT_ID = 'http://www.wikidata.org/entity/Q25267'
|
||||||
|
FAHRENHEIT_UNIT_ID = 'http://www.wikidata.org/entity/Q42289'
|
||||||
|
|
||||||
|
def self.find_wikidata_id(location_name)
|
||||||
|
return nil if location_name.blank?
|
||||||
|
|
||||||
|
uri = URI("https://www.wikidata.org/w/api.php?action=wbsearchentities&search=#{URI.encode_www_form_component(location_name)}&language=en&format=json")
|
||||||
|
req = Net::HTTP::Get.new(uri)
|
||||||
|
req['User-Agent'] = "Growstuff (https://www.growstuff.org; admin@growstuff.org)"
|
||||||
|
|
||||||
|
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
||||||
|
http.request(req)
|
||||||
|
end
|
||||||
|
|
||||||
|
data = JSON.parse(response.body)
|
||||||
|
data.dig('search', 0, 'id')
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "WikidataService.find_wikidata_id error: #{e.message}"
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.fetch_temps(wikidata_id)
|
||||||
|
return {} if wikidata_id.blank?
|
||||||
|
|
||||||
|
uri = URI("https://www.wikidata.org/w/api.php?action=wbgetentities&ids=#{wikidata_id}&props=claims&format=json")
|
||||||
|
req = Net::HTTP::Get.new(uri)
|
||||||
|
req['User-Agent'] = "Growstuff (https://www.growstuff.org; admin@growstuff.org)"
|
||||||
|
|
||||||
|
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
||||||
|
http.request(req)
|
||||||
|
end
|
||||||
|
|
||||||
|
data = JSON.parse(response.body)
|
||||||
|
claims = data.dig('entities', wikidata_id, 'claims') || {}
|
||||||
|
|
||||||
|
highest_temp = extract_temp(claims['P6591'])
|
||||||
|
lowest_temp = extract_temp(claims['P7422'])
|
||||||
|
|
||||||
|
{
|
||||||
|
highest_temp_c: highest_temp,
|
||||||
|
lowest_temp_c: lowest_temp
|
||||||
|
}
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "WikidataService.fetch_temps error: #{e.message}"
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.extract_temp(claim_data)
|
||||||
|
return nil if claim_data.blank?
|
||||||
|
|
||||||
|
# We take the first value
|
||||||
|
main_snak = claim_data.first&.dig('mainsnak')
|
||||||
|
return nil unless main_snak&.dig('datavalue', 'type') == 'quantity'
|
||||||
|
|
||||||
|
quantity_data = main_snak.dig('datavalue', 'value')
|
||||||
|
amount = quantity_data['amount'].to_f
|
||||||
|
unit = quantity_data['unit']
|
||||||
|
|
||||||
|
case unit
|
||||||
|
when CELSIUS_UNIT_ID
|
||||||
|
amount
|
||||||
|
when FAHRENHEIT_UNIT_ID
|
||||||
|
(amount - 32) * 5.0 / 9.0
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
%li.list-group-item= link_to "View all #{crop.name} harvests", crop_harvests_path(crop), class: 'card-link'
|
%li.list-group-item= link_to "View all #{crop.name} harvests", crop_harvests_path(crop), class: 'card-link'
|
||||||
- if crop.approved?
|
- if crop.approved?
|
||||||
- if current_member
|
- if current_member
|
||||||
%li.list-group-item= link_to "Harvest #{crop.name}", new_harvest_path(crop_id: crop.id), class: 'btn btn-block'
|
%li.list-group-item= link_to "Harvest #{crop.name}", new_harvest_path(harvest: { crop_id: crop.id }), class: 'btn btn-block'
|
||||||
- else
|
- else
|
||||||
%li.list-group-item.active
|
%li.list-group-item.active
|
||||||
= icon 'fas', 'user'
|
= icon 'fas', 'user'
|
||||||
|
|||||||
@@ -36,10 +36,11 @@
|
|||||||
= cute_icon
|
= cute_icon
|
||||||
= render 'predictions', crop: @crop
|
= render 'predictions', crop: @crop
|
||||||
- if @crop.all_companions.any?
|
- if @crop.all_companions.any?
|
||||||
%section.companions
|
- cache [@crop, 'companions'] do
|
||||||
%h2 Companions
|
%section.companions
|
||||||
- @crop.all_companions.each do |companion|
|
%h2 Companions
|
||||||
= render 'crops/tiny', crop: companion
|
- @crop.all_companions.each do |companion|
|
||||||
|
= render 'crops/tiny', crop: companion
|
||||||
|
|
||||||
- if crop_or_parent(@crop, :en_youtube_url).present?
|
- if crop_or_parent(@crop, :en_youtube_url).present?
|
||||||
%section.youtube
|
%section.youtube
|
||||||
|
|||||||
@@ -102,6 +102,8 @@
|
|||||||
%strong Collaborators:
|
%strong Collaborators:
|
||||||
- if can?(:create, GardenCollaborator.new(garden: @garden))
|
- if can?(:create, GardenCollaborator.new(garden: @garden))
|
||||||
= link_to "Manage", garden_garden_collaborators_path(@garden)
|
= link_to "Manage", garden_garden_collaborators_path(@garden)
|
||||||
|
- elsif current_member.present? && (collab = @garden.garden_collaborators.find_by(member: current_member))
|
||||||
|
= link_to "Leave garden", garden_garden_collaborator_path(@garden, collab), method: :delete, class: 'text-danger', data: { confirm: 'Are you sure you want to leave this garden?' }
|
||||||
- if @garden.garden_collaborators.any?
|
- if @garden.garden_collaborators.any?
|
||||||
%ul
|
%ul
|
||||||
- @garden.garden_collaborators.each do |collabator|
|
- @garden.garden_collaborators.each do |collabator|
|
||||||
@@ -122,6 +124,31 @@
|
|||||||
%strong Garden type:
|
%strong Garden type:
|
||||||
= @garden.garden_type.name
|
= @garden.garden_type.name
|
||||||
|
|
||||||
|
- if @garden.location_wikidata_id.present?
|
||||||
|
%hr
|
||||||
|
%p
|
||||||
|
%small
|
||||||
|
Data about this location from
|
||||||
|
= link_to "wikidata", "https://www.wikidata.org/wiki/#{@garden.location_wikidata_id}", target: '_blank', rel: 'noopener noreferrer'
|
||||||
|
|
||||||
|
%p
|
||||||
|
%strong Highest temperature:
|
||||||
|
- if @garden.highest_temp_c.present?
|
||||||
|
= "#{ @garden.highest_temp_c.round(1) }°C"
|
||||||
|
- else
|
||||||
|
Not known
|
||||||
|
%p
|
||||||
|
%strong Lowest temperature:
|
||||||
|
- if @garden.lowest_temp_c.present?
|
||||||
|
= "#{ @garden.lowest_temp_c.round(1) }°C"
|
||||||
|
- else
|
||||||
|
Not known
|
||||||
|
|
||||||
|
- elsif can?(:edit, @garden) && @garden.location.present?
|
||||||
|
.alert.alert-info
|
||||||
|
%p Wikidata information is missing for this location.
|
||||||
|
= button_to "Fetch Wikidata info", fetch_wikidata_garden_path(@garden), method: :post, class: 'btn btn-info btn-sm'
|
||||||
|
|
||||||
.card
|
.card
|
||||||
.card-header
|
.card-header
|
||||||
%h4 #{@garden.owner}'s gardens
|
%h4 #{@garden.owner}'s gardens
|
||||||
|
|||||||
@@ -52,6 +52,17 @@
|
|||||||
= f.select(:weight_unit, Harvest::WEIGHT_UNITS_VALUES, { include_blank: false }, class: 'form-control')
|
= f.select(:weight_unit, Harvest::WEIGHT_UNITS_VALUES, { include_blank: false }, class: 'form-control')
|
||||||
= f.text_area :description, rows: 6, label: 'Notes'
|
= f.text_area :description, rows: 6, label: 'Notes'
|
||||||
|
|
||||||
|
- if @planting.present?
|
||||||
|
.row
|
||||||
|
.col-md-12
|
||||||
|
= f.range_field :overall_rating, min: 1, max: 5, value: @planting.overall_rating, include_blank: 'Leave blank', label: 'Overall Rating - Planting', list: "rating-list", title: "How well is the planting going?"
|
||||||
|
%datalist{"id": "rating-list"}
|
||||||
|
%option{"value": "1"} Poor
|
||||||
|
%option{"value": "2"}
|
||||||
|
%option{"value": "3"}
|
||||||
|
%option{"value": "4"}
|
||||||
|
%option{"value": "5"} Great
|
||||||
|
|
||||||
.card-footer
|
.card-footer
|
||||||
.text-right= f.submit 'Save'
|
.text-right= f.submit 'Save'
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
.index-cards
|
.index-cards
|
||||||
- harvest.crop.plant_parts.order(:name).each do |plant_part|
|
- harvest.crop.plant_parts.order(:name).each do |plant_part|
|
||||||
.card
|
.card
|
||||||
= link_to harvests_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}), method: :post do
|
= link_to new_harvest_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}) do
|
||||||
.card-title.text-center
|
.card-title.text-center
|
||||||
%h3= plant_part_icon(plant_part.name)
|
%h3= plant_part_icon(plant_part.name)
|
||||||
%h3= plant_part.name
|
%h3= plant_part.name
|
||||||
@@ -22,10 +22,12 @@
|
|||||||
%h6 All Plant parts
|
%h6 All Plant parts
|
||||||
%nav.nav
|
%nav.nav
|
||||||
- PlantPart.all.order(:name).each do |plant_part|
|
- PlantPart.all.order(:name).each do |plant_part|
|
||||||
= link_to harvests_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}), method: :post, class: 'nav-link border' do
|
= link_to new_harvest_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}), class: 'nav-link border' do
|
||||||
= plant_part_icon(plant_part.name)
|
= plant_part_icon(plant_part.name)
|
||||||
= plant_part
|
= plant_part
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
%a.btn#modalHarvestButton{"data-bs-target" => "#modelHarvestForm", "data-bs-toggle" => "modal", href: ""}
|
%a.btn#modalHarvestButton{"data-bs-target" => "#modelHarvestForm", "data-bs-toggle" => "modal", href: ""}
|
||||||
= harvest_icon
|
= harvest_icon
|
||||||
Record harvest
|
Record harvest
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
%h2= t('.recently_planted')
|
%h2= t('.recently_planted')
|
||||||
- Planting.homepage_records(6).each do |planting|
|
- Planting.homepage_records(6).each do |planting|
|
||||||
|
- next unless planting['thumbnail_url'].present?
|
||||||
= link_to planting_path(slug: planting['slug']), class: 'list-group-item list-group-item-action flex-column align-items-start' do
|
= 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
|
.d-flex.w-100.justify-content-between.homepage--list-item
|
||||||
%p.mb-2
|
%p.mb-2
|
||||||
|
|||||||
@@ -32,11 +32,9 @@
|
|||||||
- else
|
- else
|
||||||
%meta{name: "description", content: "Growstuff is a community of food gardeners. Let's learn to grow food together. All our data is open data."}
|
%meta{name: "description", content: "Growstuff is a community of food gardeners. Let's learn to grow food together. All our data is open data."}
|
||||||
= csrf_meta_tags
|
= csrf_meta_tags
|
||||||
%meta{name: "vapid-public-key", content: ENV['GROWSTUFF_VAPID_PUBLIC_KEY']}
|
|
||||||
= stylesheet_link_tag "application", media: "all"
|
= stylesheet_link_tag "application", media: "all"
|
||||||
|
|
||||||
%link{ href: path_to_image("growstuff-apple-touch-icon-precomposed.png"), rel: "apple-touch-icon-precomposed" }
|
%link{ href: path_to_image("growstuff-apple-touch-icon-precomposed.png"), rel: "apple-touch-icon-precomposed" }
|
||||||
%link{ href: "https://fonts.googleapis.com/css?family=Modak|Raleway&display=swap", rel: "stylesheet" }
|
%link{ href: "https://fonts.googleapis.com/css?family=Modak|Raleway&display=swap", rel: "stylesheet" }
|
||||||
= favicon_link_tag 'favicon.ico'
|
= favicon_link_tag 'favicon.ico'
|
||||||
= serviceworker_js_tag
|
|
||||||
= tag("meta", name: "google-site-verification", content: "j249rPGdBqZ7gcShcdsSXCnGN5lqCuTISJnlQXxOfu4")
|
= tag("meta", name: "google-site-verification", content: "j249rPGdBqZ7gcShcdsSXCnGN5lqCuTISJnlQXxOfu4")
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
.card.mt-3
|
|
||||||
.card-body
|
|
||||||
%h5.card-title Notifications
|
|
||||||
%p
|
|
||||||
Install Growstuff as a Progressive Web App (PWA) to get notifications on your device.
|
|
||||||
Look for the "Add to Home Screen" option in your browser's menu.
|
|
||||||
%button.btn.btn-primary#enable-push-notifications
|
|
||||||
Enable Push Notifications
|
|
||||||
@@ -73,8 +73,6 @@
|
|||||||
|
|
||||||
= render 'members/follow_buttons', member: @member
|
= render 'members/follow_buttons', member: @member
|
||||||
|
|
||||||
= render "notifications", member: @member if can?(:update, @member)
|
|
||||||
|
|
||||||
- if can?(:destroy, @member)
|
- if can?(:destroy, @member)
|
||||||
%hr/
|
%hr/
|
||||||
= link_to admin_member_path(slug: @member.slug), method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-block btn-light text-danger' do
|
= link_to admin_member_path(slug: @member.slug), method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-block btn-light text-danger' do
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
%button.close{ type: "button", "data-bs-dismiss" => "alert" }
|
%button.close{ type: "button", "data-bs-dismiss" => "alert" }
|
||||||
%span{ "aria-hidden" => true } ×
|
%span{ "aria-hidden" => true } ×
|
||||||
%span.sr-only Close
|
%span.sr-only Close
|
||||||
= content
|
= sanitize(content)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ Rails.application.routes.draw do
|
|||||||
match '/members/:id/finish_signup' => 'members#finish_signup', via: %i(get patch), as: :finish_signup
|
match '/members/:id/finish_signup' => 'members#finish_signup', via: %i(get patch), as: :finish_signup
|
||||||
|
|
||||||
resources :authentications, only: %i(create destroy)
|
resources :authentications, only: %i(create destroy)
|
||||||
resources :push_subscriptions, only: %i(create destroy)
|
|
||||||
|
|
||||||
get "home/index"
|
get "home/index"
|
||||||
get '/community-gardens', to: 'home#community_gardens'
|
get '/community-gardens', to: 'home#community_gardens'
|
||||||
@@ -33,6 +32,7 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
resources :gardens, concerns: :has_photos, param: :slug do
|
resources :gardens, concerns: :has_photos, param: :slug do
|
||||||
get 'timeline' => 'charts/gardens#timeline', constraints: { format: 'json' }
|
get 'timeline' => 'charts/gardens#timeline', constraints: { format: 'json' }
|
||||||
|
post 'fetch_wikidata' => 'gardens#fetch_wikidata', on: :member
|
||||||
|
|
||||||
resources :garden_collaborators
|
resources :garden_collaborators
|
||||||
end
|
end
|
||||||
@@ -158,6 +158,7 @@ Rails.application.routes.draw do
|
|||||||
namespace :api do
|
namespace :api do
|
||||||
namespace :v1 do
|
namespace :v1 do
|
||||||
jsonapi_resources :activities
|
jsonapi_resources :activities
|
||||||
|
get "crops/search", to: "crops#search"
|
||||||
jsonapi_resources :crops
|
jsonapi_resources :crops
|
||||||
jsonapi_resources :gardens
|
jsonapi_resources :gardens
|
||||||
jsonapi_resources :harvests
|
jsonapi_resources :harvests
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class AddTimezoneToMembers < ActiveRecord::Migration[7.2]
|
|
||||||
def change
|
|
||||||
add_column :members, :timezone, :string
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class CreatePushSubscriptions < ActiveRecord::Migration[7.2]
|
|
||||||
def change
|
|
||||||
create_table :push_subscriptions do |t|
|
|
||||||
t.references :member, null: false, foreign_key: true
|
|
||||||
t.string :endpoint, null: false
|
|
||||||
t.string :p256dh, null: false
|
|
||||||
t.string :auth, null: false
|
|
||||||
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
add_index :push_subscriptions, :endpoint, unique: true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
class AddWikidataAndTempsToGardens < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :gardens, :location_wikidata_id, :string
|
||||||
|
add_column :gardens, :lowest_temp_c, :float
|
||||||
|
add_column :gardens, :highest_temp_c, :float
|
||||||
|
end
|
||||||
|
end
|
||||||
47
db/schema.rb
47
db/schema.rb
@@ -631,6 +631,9 @@ ActiveRecord::Schema[7.2].define(version: 2026_04_29_132911) do
|
|||||||
t.decimal "area"
|
t.decimal "area"
|
||||||
t.string "area_unit"
|
t.string "area_unit"
|
||||||
t.integer "garden_type_id"
|
t.integer "garden_type_id"
|
||||||
|
t.string "location_wikidata_id"
|
||||||
|
t.float "lowest_temp_c"
|
||||||
|
t.float "highest_temp_c"
|
||||||
t.index ["garden_type_id"], name: "index_gardens_on_garden_type_id"
|
t.index ["garden_type_id"], name: "index_gardens_on_garden_type_id"
|
||||||
t.index ["owner_id"], name: "index_gardens_on_owner_id"
|
t.index ["owner_id"], name: "index_gardens_on_owner_id"
|
||||||
t.index ["slug"], name: "index_gardens_on_slug", unique: true
|
t.index ["slug"], name: "index_gardens_on_slug", unique: true
|
||||||
@@ -786,7 +789,6 @@ ActiveRecord::Schema[7.2].define(version: 2026_04_29_132911) do
|
|||||||
t.string "facebook_handle"
|
t.string "facebook_handle"
|
||||||
t.string "bluesky_handle"
|
t.string "bluesky_handle"
|
||||||
t.string "other_url"
|
t.string "other_url"
|
||||||
t.string "timezone"
|
|
||||||
t.boolean "send_harvest_reminder", default: true, null: false
|
t.boolean "send_harvest_reminder", default: true, null: false
|
||||||
t.index ["confirmation_token"], name: "index_members_on_confirmation_token", unique: true
|
t.index ["confirmation_token"], name: "index_members_on_confirmation_token", unique: true
|
||||||
t.index ["discarded_at"], name: "index_members_on_discarded_at"
|
t.index ["discarded_at"], name: "index_members_on_discarded_at"
|
||||||
@@ -898,7 +900,6 @@ ActiveRecord::Schema[7.2].define(version: 2026_04_29_132911) do
|
|||||||
t.integer "harvests_count", default: 0
|
t.integer "harvests_count", default: 0
|
||||||
t.integer "likes_count", default: 0
|
t.integer "likes_count", default: 0
|
||||||
t.boolean "failed", default: false, null: false
|
t.boolean "failed", default: false, null: false
|
||||||
t.boolean "from_other_source"
|
|
||||||
t.integer "overall_rating"
|
t.integer "overall_rating"
|
||||||
t.index ["crop_id"], name: "index_plantings_on_crop_id"
|
t.index ["crop_id"], name: "index_plantings_on_crop_id"
|
||||||
t.index ["garden_id"], name: "index_plantings_on_garden_id"
|
t.index ["garden_id"], name: "index_plantings_on_garden_id"
|
||||||
@@ -922,43 +923,6 @@ ActiveRecord::Schema[7.2].define(version: 2026_04_29_132911) do
|
|||||||
t.index ["slug"], name: "index_posts_on_slug", unique: true
|
t.index ["slug"], name: "index_posts_on_slug", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "problem_posts", force: :cascade do |t|
|
|
||||||
t.bigint "problem_id"
|
|
||||||
t.bigint "post_id"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.datetime "updated_at", null: false
|
|
||||||
t.index ["post_id"], name: "index_problem_posts_on_post_id"
|
|
||||||
t.index ["problem_id", "post_id"], name: "index_problem_posts_on_problem_id_and_post_id", unique: true
|
|
||||||
t.index ["problem_id"], name: "index_problem_posts_on_problem_id"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "problems", force: :cascade do |t|
|
|
||||||
t.string "name"
|
|
||||||
t.string "reason_for_rejection"
|
|
||||||
t.string "rejection_notes"
|
|
||||||
t.string "approval_status", default: "pending", null: false
|
|
||||||
t.bigint "requester_id"
|
|
||||||
t.bigint "creator_id"
|
|
||||||
t.string "slug"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.datetime "updated_at", null: false
|
|
||||||
t.index ["creator_id"], name: "index_problems_on_creator_id"
|
|
||||||
t.index ["name"], name: "index_problems_on_name"
|
|
||||||
t.index ["requester_id"], name: "index_problems_on_requester_id"
|
|
||||||
t.index ["slug"], name: "index_problems_on_slug"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "push_subscriptions", force: :cascade do |t|
|
|
||||||
t.bigint "member_id", null: false
|
|
||||||
t.string "endpoint", null: false
|
|
||||||
t.string "p256dh", null: false
|
|
||||||
t.string "auth", null: false
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.datetime "updated_at", null: false
|
|
||||||
t.index ["endpoint"], name: "index_push_subscriptions_on_endpoint", unique: true
|
|
||||||
t.index ["member_id"], name: "index_push_subscriptions_on_member_id"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "roles", id: :serial, force: :cascade do |t|
|
create_table "roles", id: :serial, force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.text "description"
|
t.text "description"
|
||||||
@@ -1031,10 +995,5 @@ ActiveRecord::Schema[7.2].define(version: 2026_04_29_132911) do
|
|||||||
add_foreign_key "photo_associations", "crops"
|
add_foreign_key "photo_associations", "crops"
|
||||||
add_foreign_key "photo_associations", "photos"
|
add_foreign_key "photo_associations", "photos"
|
||||||
add_foreign_key "plantings", "seeds", column: "parent_seed_id", name: "parent_seed", on_delete: :nullify
|
add_foreign_key "plantings", "seeds", column: "parent_seed_id", name: "parent_seed", on_delete: :nullify
|
||||||
add_foreign_key "problem_posts", "posts"
|
|
||||||
add_foreign_key "problem_posts", "problems"
|
|
||||||
add_foreign_key "problems", "members", column: "creator_id"
|
|
||||||
add_foreign_key "problems", "members", column: "requester_id"
|
|
||||||
add_foreign_key "push_subscriptions", "members"
|
|
||||||
add_foreign_key "seeds", "plantings", column: "parent_planting_id", name: "parent_planting", on_delete: :nullify
|
add_foreign_key "seeds", "plantings", column: "parent_planting_id", name: "parent_planting", on_delete: :nullify
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,9 +65,3 @@ MAILGUN_SMTP_SERVER=""
|
|||||||
# In production, replace them with real ones
|
# In production, replace them with real ones
|
||||||
RECAPTCHA_SITE_KEY="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"
|
RECAPTCHA_SITE_KEY="6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"
|
||||||
RECAPTCHA_SECRET_KEY="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
|
RECAPTCHA_SECRET_KEY="6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
|
||||||
|
|
||||||
# VAPID keys for web push notifications
|
|
||||||
# These are insecure and should be replaced with real keys in production
|
|
||||||
# Generate new keys with `bundle exec rake webpush:generate_keys`
|
|
||||||
GROWSTUFF_VAPID_PUBLIC_KEY="BFf_pM3_3q0g1hIUiWf_nQdYj524I4E-mp3jW_j_7X-B-xWpW-j_8X_8X_8X_8X_8X_8X_8X_8X_8"
|
|
||||||
GROWSTUFF_VAPID_PRIVATE_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
||||||
|
|||||||
@@ -115,6 +115,21 @@ describe HarvestsController, :search do
|
|||||||
|
|
||||||
it { expect(Harvest.last.planting.id).to eq(planting.id) }
|
it { expect(Harvest.last.planting.id).to eq(planting.id) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "updates planting rating" do
|
||||||
|
let(:planting) { create(:planting, owner_id: member.id, garden: member.gardens.first) }
|
||||||
|
|
||||||
|
it "updates the planting rating when provided" do
|
||||||
|
post :create, params: {
|
||||||
|
harvest: valid_attributes.merge(
|
||||||
|
planting_id: planting.id,
|
||||||
|
crop_id: planting.crop_id,
|
||||||
|
overall_rating: 4
|
||||||
|
)
|
||||||
|
}
|
||||||
|
expect(planting.reload.overall_rating).to eq(4)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "with invalid params" do
|
describe "with invalid params" do
|
||||||
@@ -171,6 +186,18 @@ describe HarvestsController, :search do
|
|||||||
|
|
||||||
it { expect(response).to redirect_to(harvest) }
|
it { expect(response).to redirect_to(harvest) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "updates planting rating" do
|
||||||
|
let(:planting) { create(:planting, owner_id: member.id, garden: member.gardens.first) }
|
||||||
|
let(:harvest) do
|
||||||
|
create(:harvest, valid_attributes.merge(planting_id: planting.id, crop_id: planting.crop_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
it "updates the planting rating when provided" do
|
||||||
|
put :update, params: { slug: harvest.to_param, harvest: { overall_rating: 3 } }
|
||||||
|
expect(planting.reload.overall_rating).to eq(3)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "with invalid params" do
|
describe "with invalid params" do
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ describe "Harvesting a crop", :js, :search do
|
|||||||
visit crop_path(maize)
|
visit crop_path(maize)
|
||||||
click_link "Record harvest"
|
click_link "Record harvest"
|
||||||
click_link plant_part.name
|
click_link plant_part.name
|
||||||
|
# We then navigate to the new_harvest_path, and save.
|
||||||
|
click_button "Save"
|
||||||
end
|
end
|
||||||
|
|
||||||
it { expect(page).to have_content "harvest was successfully created." }
|
it { expect(page).to have_content "harvest was successfully created." }
|
||||||
@@ -69,9 +71,22 @@ describe "Harvesting a crop", :js, :search do
|
|||||||
click_link plant_part.name
|
click_link plant_part.name
|
||||||
end
|
end
|
||||||
|
|
||||||
it { expect(page).to have_content "harvest was successfully created." }
|
it "saves" do
|
||||||
it { expect(page).to have_content planting.garden.name }
|
# We then navigate to the new_harvest_path, and save.
|
||||||
it { expect(page).to have_content "maize" }
|
click_button "Save"
|
||||||
|
|
||||||
|
expect(page).to have_content "harvest was successfully created."
|
||||||
|
expect(page).to have_content planting.garden.name
|
||||||
|
expect(page).to have_content "maize"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "updates the planting rating" do
|
||||||
|
find_by_id('harvest_overall_rating').set 4
|
||||||
|
click_button "Save"
|
||||||
|
|
||||||
|
expect(page).to have_content "harvest was successfully created."
|
||||||
|
expect(planting.reload.overall_rating).to eq 4
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "Editing a harvest" do
|
context "Editing a harvest" do
|
||||||
|
|||||||
32
spec/models/garden_collaborator_ability_spec.rb
Normal file
32
spec/models/garden_collaborator_ability_spec.rb
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
require 'cancan/matchers'
|
||||||
|
|
||||||
|
describe Ability do
|
||||||
|
let(:member) { create(:member) }
|
||||||
|
let(:ability) { described_class.new(member) }
|
||||||
|
|
||||||
|
context 'garden collaborators' do
|
||||||
|
let(:garden) { create(:garden) }
|
||||||
|
let(:garden_collaborator) { create(:garden_collaborator, garden: garden, member: member) }
|
||||||
|
let(:other_member) { create(:member) }
|
||||||
|
let(:other_garden_collaborator) { create(:garden_collaborator, garden: garden, member: other_member) }
|
||||||
|
|
||||||
|
it 'can remove themselves as a collaborator' do
|
||||||
|
expect(ability).to be_able_to(:destroy, garden_collaborator)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'cannot remove others as a collaborator if not garden owner' do
|
||||||
|
expect(ability).not_to be_able_to(:destroy, other_garden_collaborator)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'as garden owner' do
|
||||||
|
let(:garden) { create(:garden, owner: member) }
|
||||||
|
|
||||||
|
it 'can remove others as a collaborator' do
|
||||||
|
expect(ability).to be_able_to(:destroy, other_garden_collaborator)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
35
spec/requests/api/v1/crops_search_spec.rb
Normal file
35
spec/requests/api/v1/crops_search_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Crops Search' do
|
||||||
|
subject { JSON.parse response.body }
|
||||||
|
|
||||||
|
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
|
||||||
|
let!(:cabbage) { create(:crop, name: 'Cabbage', approval_status: 'approved') }
|
||||||
|
let!(:apple) { create(:crop, name: 'Apple', approval_status: 'approved') }
|
||||||
|
|
||||||
|
describe 'GET /api/v1/crops/search' do
|
||||||
|
before do
|
||||||
|
Crop.reindex
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns crops matching the search term' do
|
||||||
|
get '/api/v1/crops/search', params: { term: 'Cabbage' }, headers: headers
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(subject['data'].size).to eq(1)
|
||||||
|
expect(subject['data'].first['attributes']['name']).to eq('Cabbage')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns empty data if no crops match' do
|
||||||
|
get '/api/v1/crops/search', params: { term: 'NonExistent' }, headers: headers
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(subject['data']).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes meta information' do
|
||||||
|
get '/api/v1/crops/search', params: { term: 'Cabbage' }, headers: headers
|
||||||
|
expect(subject['meta']).to include('record_count' => 1, 'page_count' => 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,6 +9,85 @@
|
|||||||
"/crops": {
|
"/crops": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "crops List",
|
"summary": "crops List",
|
||||||
|
"/crops/search": {
|
||||||
|
"get": {
|
||||||
|
"summary": "crops Search",
|
||||||
|
"tags": [
|
||||||
|
"crops"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/vnd.api+json"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "term",
|
||||||
|
"in": "query",
|
||||||
|
"type": "string",
|
||||||
|
"description": "Search term",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "page[number]",
|
||||||
|
"in": "query",
|
||||||
|
"type": "string",
|
||||||
|
"description": "Page num",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "page[size]",
|
||||||
|
"in": "query",
|
||||||
|
"type": "string",
|
||||||
|
"description": "Page size",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Get search results",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Type"
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"record_count": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"page_count": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"crops"
|
"crops"
|
||||||
],
|
],
|
||||||
|
|||||||
18
yarn.lock
18
yarn.lock
@@ -735,9 +735,9 @@ fast-levenshtein@^2.0.6:
|
|||||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||||
|
|
||||||
fast-uri@^3.0.1:
|
fast-uri@^3.0.1:
|
||||||
version "3.1.0"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
|
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec"
|
||||||
integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
|
integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==
|
||||||
|
|
||||||
fastq@^1.6.0:
|
fastq@^1.6.0:
|
||||||
version "1.20.1"
|
version "1.20.1"
|
||||||
@@ -915,9 +915,9 @@ inherits@2, inherits@~2.0.3:
|
|||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
ip-address@^10.0.1:
|
ip-address@^10.0.1:
|
||||||
version "10.1.0"
|
version "10.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4"
|
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.2.0.tgz#805fc178b20c518bd4c8548b24fe30892d7f3206"
|
||||||
integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==
|
integrity sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==
|
||||||
|
|
||||||
is-arrayish@^0.2.1:
|
is-arrayish@^0.2.1:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
@@ -1396,9 +1396,9 @@ supports-preserve-symlinks-flag@^1.0.0:
|
|||||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||||
|
|
||||||
systeminformation@^5.25.11:
|
systeminformation@^5.25.11:
|
||||||
version "5.31.5"
|
version "5.31.6"
|
||||||
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.31.5.tgz#e839fa6b40620a8bee010eb9d9d55c2d5f7042c8"
|
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.31.6.tgz#2da4979a7262974fd068a3a306ded30aed6127c0"
|
||||||
integrity sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==
|
integrity sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA==
|
||||||
|
|
||||||
to-regex-range@^5.0.1:
|
to-regex-range@^5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user