Compare commits

..
Author SHA1 Message Date
Daniel O'Connor 7bb632c3c1 Merge branch 'dev' into feature/set-location 2026-05-03 12:56:28 +09:30
google-labs-jules[bot] 80e724fefe feat: Add set location feature with privacy enhancements
This commit introduces a new feature that allows members to set their location using two methods: a "Find my location" button that uses the browser's Geolocation API, and a Leaflet.js map for manual marker placement.

Key changes:
- Adds a new `set_location` page for members to update their location.
- Integrates a Leaflet.js map for visual location selection.
- Implements a "Find my location" button using the browser's Geolocation API.
- Uses reverse geocoding to determine a user's suburb/city/town from coordinates.
- Enhances privacy by rounding latitude and longitude to two decimal places, reducing location accuracy.
- Provides a fallback mechanism for when reverse geocoding fails, storing a generic location string.
- Adds comprehensive feature tests for the new functionality.
2025-09-01 10:16:20 +00:00
64 changed files with 460 additions and 586 deletions

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
FROM mcr.microsoft.com/devcontainers/ruby:3.4-trixie
# Install Rails
RUN gem install rails:8.0.5
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
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+2 -2
View File
@@ -6,7 +6,7 @@ jobs:
contributors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v6
- name: Install ruby version specified in .ruby-version
uses: ruby/setup-ruby@v1
with:
@@ -53,7 +53,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Configure sysctl limits
run: |
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
+1 -1
View File
@@ -25,7 +25,7 @@ ENV RAILS_ENV=production \
WORKDIR /app
# Install gems
COPY Gemfile Gemfile.lock .ruby-version ./
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install --jobs 4 --retry 3
+1 -1
View File
@@ -5,7 +5,7 @@ source 'https://rubygems.org'
# Match ruby version in .ruby-version
ruby File.read('.ruby-version')
gem 'rails', '~> 8.0.0'
gem 'rails', '~> 7.2.0'
# Keeping old sprockets
# https://github.com/rails/sprockets-rails/issues/444#issuecomment-637817050
+98 -97
View File
@@ -33,46 +33,49 @@ GEM
GEM
remote: https://rubygems.org/
specs:
actioncable (8.0.5)
actionpack (= 8.0.5)
activesupport (= 8.0.5)
actioncable (7.2.3.1)
actionpack (= 7.2.3.1)
activesupport (= 7.2.3.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.0.5)
actionpack (= 8.0.5)
activejob (= 8.0.5)
activerecord (= 8.0.5)
activestorage (= 8.0.5)
activesupport (= 8.0.5)
actionmailbox (7.2.3.1)
actionpack (= 7.2.3.1)
activejob (= 7.2.3.1)
activerecord (= 7.2.3.1)
activestorage (= 7.2.3.1)
activesupport (= 7.2.3.1)
mail (>= 2.8.0)
actionmailer (8.0.5)
actionpack (= 8.0.5)
actionview (= 8.0.5)
activejob (= 8.0.5)
activesupport (= 8.0.5)
actionmailer (7.2.3.1)
actionpack (= 7.2.3.1)
actionview (= 7.2.3.1)
activejob (= 7.2.3.1)
activesupport (= 7.2.3.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.0.5)
actionview (= 8.0.5)
activesupport (= 8.0.5)
actionpack (7.2.3.1)
actionview (= 7.2.3.1)
activesupport (= 7.2.3.1)
cgi
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
racc
rack (>= 2.2.4, < 3.3)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.0.5)
actionpack (= 8.0.5)
activerecord (= 8.0.5)
activestorage (= 8.0.5)
activesupport (= 8.0.5)
actiontext (7.2.3.1)
actionpack (= 7.2.3.1)
activerecord (= 7.2.3.1)
activestorage (= 7.2.3.1)
activesupport (= 7.2.3.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.0.5)
activesupport (= 8.0.5)
actionview (7.2.3.1)
activesupport (= 7.2.3.1)
builder (~> 3.1)
cgi
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
@@ -86,22 +89,22 @@ GEM
active_utils (3.6.0)
activesupport (>= 4.2)
i18n
activejob (8.0.5)
activesupport (= 8.0.5)
activejob (7.2.3.1)
activesupport (= 7.2.3.1)
globalid (>= 0.3.6)
activemodel (8.0.5)
activesupport (= 8.0.5)
activerecord (8.0.5)
activemodel (= 8.0.5)
activesupport (= 8.0.5)
activemodel (7.2.3.1)
activesupport (= 7.2.3.1)
activerecord (7.2.3.1)
activemodel (= 7.2.3.1)
activesupport (= 7.2.3.1)
timeout (>= 0.4.0)
activestorage (8.0.5)
actionpack (= 8.0.5)
activejob (= 8.0.5)
activerecord (= 8.0.5)
activesupport (= 8.0.5)
activestorage (7.2.3.1)
actionpack (= 7.2.3.1)
activejob (= 7.2.3.1)
activerecord (= 7.2.3.1)
activesupport (= 7.2.3.1)
marcel (~> 1.0)
activesupport (8.0.5)
activesupport (7.2.3.1)
base64
benchmark (>= 0.3)
bigdecimal
@@ -110,18 +113,17 @@ GEM
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
minitest (>= 5.1, < 6)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
autoprefixer-rails (10.4.16.0)
execjs (~> 2)
aws-eventstream (1.4.0)
aws-partitions (1.1261.0)
aws-sdk-core (3.252.0)
aws-partitions (1.1240.0)
aws-sdk-core (3.245.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@@ -129,11 +131,11 @@ GEM
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.129.0)
aws-sdk-core (~> 3, >= 3.248.0)
aws-sdk-kms (1.123.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.226.0)
aws-sdk-core (~> 3, >= 3.248.0)
aws-sdk-s3 (1.220.0)
aws-sdk-core (~> 3, >= 3.244.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
@@ -175,7 +177,7 @@ GEM
actionpack (>= 6.1)
activemodel (>= 6.1)
builder (3.3.0)
bullet (8.1.3)
bullet (8.1.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
byebug (13.0.0)
@@ -203,6 +205,7 @@ GEM
image_processing (~> 1.1)
marcel (~> 1.0.0)
ssrf_filter (~> 1.0)
cgi (0.5.1)
chartkick (5.2.1)
childprocess (5.1.0)
logger (~> 1.5)
@@ -218,7 +221,7 @@ GEM
coffee-script-source (1.12.2)
comfy_bootstrap_form (4.0.9)
rails (>= 5.0.0)
concurrent-ruby (1.3.7)
concurrent-ruby (1.3.6)
connection_pool (2.5.5)
crass (1.0.6)
crowdin-api (1.12.0)
@@ -233,7 +236,7 @@ GEM
csv_shaper (1.4.0)
activesupport (>= 3.0.0)
csv
dalli (5.0.5)
dalli (5.0.2)
logger
database_cleaner (2.1.0)
database_cleaner-active_record (>= 2, < 3)
@@ -244,15 +247,15 @@ GEM
date (3.5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (5.0.4)
devise (5.0.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 7.0)
responders
warden (~> 1.2.3)
diff-lcs (1.6.2)
discard (2.0.0)
activerecord (>= 7.0, < 9.0)
discard (1.4.0)
activerecord (>= 4.2, < 9.0)
domain_name (0.6.20240107)
dotenv (3.2.0)
dotenv-rails (3.2.0)
@@ -283,18 +286,18 @@ GEM
railties (>= 6.1.0)
faker (3.8.0)
i18n (>= 1.8.11, < 2)
faraday (2.14.3)
faraday (2.14.1)
faraday-net_http (>= 2.0, < 3.5)
json
logger
faraday-net_http (3.4.4)
faraday-net_http (3.4.2)
net-http (~> 0.5)
ffi (1.17.3)
ffi (1.17.3-x86_64-linux-gnu)
flickraw (0.9.10)
font-awesome-sass (5.15.1)
sassc (>= 1.11)
friendly_id (5.7.0)
friendly_id (5.6.0)
activerecord (>= 4.0.0)
gbifrb (0.2.0)
geocoder (1.8.6)
@@ -319,7 +322,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.74.0)
haml_lint (0.73.0)
haml (>= 5.0)
parallel (>= 1.10)
rainbow
@@ -336,7 +339,7 @@ GEM
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.15.2)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
i18n-tasks (1.1.2)
activesupport (>= 4.0.2)
@@ -352,7 +355,7 @@ GEM
terminal-table (>= 1.5.1)
i18n_data (1.1.0)
simple_po_parser (~> 1.1)
icalendar (2.12.3)
icalendar (2.12.2)
base64
ice_cube (~> 0.16)
logger
@@ -363,7 +366,7 @@ GEM
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.8.2)
irb (1.18.0)
irb (1.17.0)
pp (>= 0.6.0)
prism (>= 1.3.0)
rdoc (>= 4.0.0)
@@ -373,7 +376,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.19.9)
json (2.19.4)
json-schema (6.2.0)
addressable (~> 2.8)
bigdecimal (>= 3.1, < 5)
@@ -432,16 +435,14 @@ GEM
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (6.0.6)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.3)
minitest (5.27.0)
msgpack (1.8.0)
multi_json (1.19.1)
multi_xml (0.8.1)
bigdecimal (>= 3.1, < 5)
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.6.4.1)
net-imap (0.5.12)
date
net-protocol
net-pop (0.1.2)
@@ -452,13 +453,13 @@ GEM
net-protocol
netrc (0.11.0)
nio4r (2.7.5)
nokogiri (1.19.4)
nokogiri (1.19.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.19.4-x86_64-linux-gnu)
nokogiri (1.19.2-x86_64-linux-gnu)
racc (~> 1.4)
oauth (0.5.6)
oj (3.17.3)
oj (3.17.0)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (1.9.2)
@@ -497,7 +498,7 @@ GEM
date
stringio
public_suffix (7.0.5)
puma (8.0.2)
puma (8.0.1)
nio4r (~> 2.0)
query_diet (0.7.3)
racc (1.8.1)
@@ -516,20 +517,20 @@ GEM
rackup (1.0.1)
rack (< 3)
webrick
rails (8.0.5)
actioncable (= 8.0.5)
actionmailbox (= 8.0.5)
actionmailer (= 8.0.5)
actionpack (= 8.0.5)
actiontext (= 8.0.5)
actionview (= 8.0.5)
activejob (= 8.0.5)
activemodel (= 8.0.5)
activerecord (= 8.0.5)
activestorage (= 8.0.5)
activesupport (= 8.0.5)
rails (7.2.3.1)
actioncable (= 7.2.3.1)
actionmailbox (= 7.2.3.1)
actionmailer (= 7.2.3.1)
actionpack (= 7.2.3.1)
actiontext (= 7.2.3.1)
actionview (= 7.2.3.1)
activejob (= 7.2.3.1)
activemodel (= 7.2.3.1)
activerecord (= 7.2.3.1)
activestorage (= 7.2.3.1)
activesupport (= 7.2.3.1)
bundler (>= 1.15.0)
railties (= 8.0.5)
railties (= 7.2.3.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -541,17 +542,18 @@ GEM
rails-html-sanitizer (1.7.0)
loofah (~> 2.25)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (8.1.0)
rails-i18n (7.0.10)
i18n (>= 0.7, < 2)
railties (>= 8.0.0, < 9)
railties (>= 6.0.0, < 8)
rails_12factor (0.0.3)
rails_serve_static_assets
rails_stdout_logging
rails_serve_static_assets (0.0.5)
rails_stdout_logging (0.0.5)
railties (8.0.5)
actionpack (= 8.0.5)
activesupport (= 8.0.5)
railties (7.2.3.1)
actionpack (= 7.2.3.1)
activesupport (= 7.2.3.1)
cgi
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -628,7 +630,7 @@ GEM
rswag-ui (2.17.0)
actionpack (>= 5.2, < 8.2)
railties (>= 5.2, < 8.2)
rubocop (1.88.0)
rubocop (1.86.1)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -642,13 +644,13 @@ GEM
rubocop-ast (1.49.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-capybara (3.0.0)
rubocop-capybara (2.23.0)
lint_roller (~> 1.1)
rubocop (~> 1.81)
rubocop-factory_bot (2.28.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rails (2.35.5)
rubocop-rails (2.34.3)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
@@ -657,10 +659,9 @@ GEM
rubocop-rake (0.7.1)
lint_roller (~> 1.1)
rubocop (>= 1.72.1)
rubocop-rspec (3.10.2)
rubocop-rspec (3.9.0)
lint_roller (~> 1.1)
regexp_parser (>= 2.0)
rubocop (~> 1.86, >= 1.86.2)
rubocop (~> 1.81)
rubocop-rspec_rails (2.32.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
@@ -669,7 +670,7 @@ GEM
ruby-units (4.1.0)
ruby-vips (2.2.1)
ffi (~> 1.12)
rubyzip (3.4.0)
rubyzip (3.2.2)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
@@ -689,7 +690,7 @@ GEM
activemodel (>= 6.1)
hashie
securerandom (0.4.1)
selenium-webdriver (4.45.0)
selenium-webdriver (4.43.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@@ -724,7 +725,7 @@ GEM
thread_safe (0.3.6)
tilt (2.7.0)
timecop (0.9.11)
timeout (0.6.1)
timeout (0.5.0)
tsort (0.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
@@ -845,7 +846,7 @@ DEPENDENCIES
rack-attack
rack-cors
rack-protection (>= 2.0.1)
rails (~> 8.0.0)
rails (~> 7.2.0)
rails-assets-leaflet.markercluster!
rails-controller-testing
rails_12factor
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "growstuff",
"stack": "heroku-26",
"stack": "heroku-18",
"description": "Open data project for small-scale food growers",
"scripts": {
"postdeploy": "bundle exec rails db:seed"
+41
View File
@@ -30,3 +30,44 @@ if (document.getElementById("membermap") !== null) {
});
}
$(document).on('click', '#find-me', function(e) {
e.preventDefault();
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(position) {
$('#member_latitude').val(position.coords.latitude);
$('#member_longitude').val(position.coords.longitude);
updateMap(position.coords.latitude, position.coords.longitude);
});
} else {
alert("Geolocation is not supported by this browser.");
}
});
if (document.getElementById("map") !== null) {
var map = L.map('map').setView([0, 0], 2);
var marker;
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 18
}).addTo(map);
map.on('click', function(e) {
updateMarker(e.latlng);
$('#member_latitude').val(e.latlng.lat);
$('#member_longitude').val(e.latlng.lng);
});
function updateMarker(latlng) {
if (marker) {
map.removeLayer(marker);
}
marker = L.marker(latlng).addTo(map);
}
function updateMap(lat, lng) {
map.setView([lat, lng], 13);
updateMarker(L.latLng(lat, lng));
}
}
@@ -3,36 +3,6 @@
module Api
module V1
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
+1 -12
View File
@@ -57,23 +57,12 @@ class GardensController < DataController
redirect_to(member_gardens_path(@garden.owner))
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
def garden_params
params.require(:garden).permit(
:name, :slug, :description, :active,
:location, :latitude, :longitude, :area, :area_unit, :garden_type_id,
:location_wikidata_id, :lowest_temp_c, :highest_temp_c
:location, :latitude, :longitude, :area, :area_unit, :garden_type_id
)
end
end
+6 -22
View File
@@ -38,9 +38,9 @@ class HarvestsController < DataController
end
def new
@harvest = Harvest.new(new_harvest_params.merge(harvested_at: Time.zone.today))
@planting = @harvest.planting
@crop = @harvest.crop
@harvest = Harvest.new(harvested_at: Time.zone.today)
@planting = Planting.find_by(slug: params[:planting_slug]) if params[:planting_slug]
@crop = Crop.find_by(id: params[:crop_id])
respond_with(@harvest)
end
@@ -52,7 +52,7 @@ class HarvestsController < DataController
def create
@harvest.crop_id = @harvest.planting.crop_id if @harvest.planting_id
@harvest.harvested_at = Time.zone.now if @harvest.harvested_at.blank?
update_planting_rating if @harvest.save
@harvest.save
if params[:return] == 'planting'
respond_with(@harvest, location: @harvest.planting)
else
@@ -61,7 +61,7 @@ class HarvestsController < DataController
end
def update
update_planting_rating if @harvest.update(harvest_params)
@harvest.update(harvest_params)
respond_with(@harvest)
end
@@ -76,17 +76,7 @@ class HarvestsController < DataController
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)
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)
:plant_part_id, :slug, :si_weight)
.merge(owner_id: current_member.id)
end
@@ -113,10 +103,4 @@ class HarvestsController < DataController
@harvest.planting.update_harvest_days!
@harvest.crop.update_harvest_medians
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
+2 -2
View File
@@ -11,7 +11,7 @@ class LikesController < ApplicationController
likeable_id: params[:id]
)
if @like.likeable && @like.save
@like.likeable.reindex(refresh: true) if @like.likeable.respond_to?(:reindex)
@like.likeable.reindex(refresh: true)
success(@like, liked_by_member: true, status_code: :created)
else
failed(@like, message: t('messages.unable_to_like'))
@@ -26,7 +26,7 @@ class LikesController < ApplicationController
)
if @like&.destroy
@like.likeable.reindex(refresh: true) if @like.likeable.respond_to?(:reindex)
@like.likeable.reindex(refresh: true)
success(@like, liked_by_member: false, status_code: :ok)
else
failed(@like, message: t('messages.unable_to_unlike'))
+32 -2
View File
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class MembersController < ApplicationController
load_and_authorize_resource except: %i(finish_signup unsubscribe view_follows view_followers show)
load_and_authorize_resource except: %i(finish_signup unsubscribe view_follows view_followers show set_location update_location)
skip_authorize_resource only: %i(nearby unsubscribe finish_signup)
respond_to :html, :json, :rss
@@ -86,6 +86,36 @@ class MembersController < ApplicationController
end
end
def set_location
@member = Member.find_by_slug!(params[:id])
authorize! :update, @member
end
def update_location
@member = Member.find_by_slug!(params[:id])
authorize! :update, @member
if params[:member][:latitude].present? && params[:member][:longitude].present?
lat = params[:member][:latitude].to_f.round(2)
lng = params[:member][:longitude].to_f.round(2)
params[:member][:latitude] = lat
params[:member][:longitude] = lng
results = Geocoder.search([lat, lng])
if results.first
params[:member][:location] = results.first.city || results.first.town || results.first.village || results.first.hamlet
else
params[:member][:location] = "Location near #{lat}, #{lng}"
end
end
if @member.update(member_params)
redirect_to member_path(@member), notice: 'Location updated.'
else
render :set_location
end
end
private
EMAIL_TYPE_STRING = {
@@ -95,7 +125,7 @@ class MembersController < ApplicationController
}.freeze
def member_params
params.require(:member).permit(:login_name, :tos_agreement, :email, :newsletter, :send_harvest_reminder)
params.require(:member).permit(:login_name, :tos_agreement, :email, :newsletter, :location, :latitude, :longitude, :send_harvest_reminder)
end
def member_json_fields
+19 -11
View File
@@ -9,17 +9,13 @@ class PhotosController < ApplicationController
responders :flash
def index
@photos = if @crop
@crop.photos
elsif @planting
@planting.photos
else
Photo.all
end
@photos = @photos.includes(:owner)
.order(created_at: :desc)
.paginate(page: params[:page], per_page: Photo.per_page)
@photos = Photo.search(
load: false,
boost_by: [:created_at],
where: index_where_clause,
page: params[:page],
limit: Photo.per_page
)
respond_with(@photos)
end
@@ -118,6 +114,18 @@ class PhotosController < ApplicationController
end
end
def index_where_clause
if params[:crop_slug]
{ crops: @crop.id }
elsif params[:planting_id]
{ planting_id: @planting.id }
elsif params[:planting_slug]
{ plantings: @planting.id }
else
{}
end
end
def set_crop_and_planting
@crop = Crop.find params[:crop_slug] if params[:crop_slug]
@planting = Planting.find params[:planting_id] if params[:planting_id]
-1
View File
@@ -123,7 +123,6 @@ class Ability
can :create, 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, member_id: member.id
can :create, Activity
can :update, Activity, owner_id: member.id
+37
View File
@@ -0,0 +1,37 @@
# frozen_string_literal: true
module SearchPhotos
extend ActiveSupport::Concern
included do
searchkick merge_mappings: true,
settings: { number_of_shards: 1, number_of_replicas: 0 },
mappings: {
properties: {
title: { type: :text },
created_at: { type: :integer }
}
}
def search_data
{
id:,
title:,
thumbnail_url:,
fullsize_url:,
# crops
crops: crops.pluck(:id),
# likes
liked_by_members_names:,
# owner
owner_id:,
owner_login_name:,
owner_slug:,
# counts
likes_count:,
created_at: created_at.to_i
}
end
end
end
+3 -5
View File
@@ -165,11 +165,9 @@ class Crop < ApplicationRecord
end
def all_companions
@all_companions ||= if parent
(companions + parent.all_companions).uniq
else
companions
end
return companions unless parent
(companions + parent.all_companions).uniq
end
before_destroy :destroy_reverse_companionships
-14
View File
@@ -21,7 +21,6 @@ class Garden < ApplicationRecord
after_validation :cleanup_area
after_validation :geocode
after_validation :empty_unwanted_geocodes
after_validation :populate_wikidata_info, if: :will_save_change_to_location?
after_save :mark_inactive_garden_plantings_as_finished
scope :active, -> { where(active: true) }
@@ -93,19 +92,6 @@ class Garden < ApplicationRecord
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
def strip_blanks
-2
View File
@@ -8,8 +8,6 @@ class Harvest < ApplicationRecord
include SearchHarvests
include Likeable
attr_accessor :overall_rating
friendly_id :harvest_slug, use: %i(slugged finders)
# Constants
+1
View File
@@ -3,6 +3,7 @@
class Photo < ApplicationRecord
include Likeable
include Ownable
include SearchPhotos
PHOTO_CAPABLE = %w(Garden Planting Harvest Seed Post Crop).freeze
+2 -2
View File
@@ -2,7 +2,7 @@
class CropSearchService
# Crop.search(string)
def self.search(query, page: 1, per_page: 12, current_member: nil, **options)
def self.search(query, page: 1, per_page: 12, current_member: nil)
search_params = {
page:,
per_page:,
@@ -12,7 +12,7 @@ class CropSearchService
includes: %i(scientific_names alternate_names),
misspellings: { edit_distance: 2 },
load: false
}.merge(options)
}
# prioritise crops the member has planted
search_params[:boost_where] = { planters_ids: current_member.id } if current_member
-74
View File
@@ -1,74 +0,0 @@
# 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
+1 -1
View File
@@ -17,7 +17,7 @@
%li.list-group-item= link_to "View all #{crop.name} harvests", crop_harvests_path(crop), class: 'card-link'
- if crop.approved?
- if current_member
%li.list-group-item= link_to "Harvest #{crop.name}", new_harvest_path(harvest: { crop_id: crop.id }), class: 'btn btn-block'
%li.list-group-item= link_to "Harvest #{crop.name}", new_harvest_path(crop_id: crop.id), class: 'btn btn-block'
- else
%li.list-group-item.active
= icon 'fas', 'user'
+4 -5
View File
@@ -36,11 +36,10 @@
= cute_icon
= render 'predictions', crop: @crop
- if @crop.all_companions.any?
- cache [@crop, 'companions'] do
%section.companions
%h2 Companions
- @crop.all_companions.each do |companion|
= render 'crops/tiny', crop: companion
%section.companions
%h2 Companions
- @crop.all_companions.each do |companion|
= render 'crops/tiny', crop: companion
- if crop_or_parent(@crop, :en_youtube_url).present?
%section.youtube
-27
View File
@@ -102,8 +102,6 @@
%strong Collaborators:
- if can?(:create, GardenCollaborator.new(garden: @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?
%ul
- @garden.garden_collaborators.each do |collabator|
@@ -124,31 +122,6 @@
%strong Garden type:
= @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-header
%h4 #{@garden.owner}'s gardens
-11
View File
@@ -52,17 +52,6 @@
= f.select(:weight_unit, Harvest::WEIGHT_UNITS_VALUES, { include_blank: false }, class: 'form-control')
= 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
.text-right= f.submit 'Save'
+2 -4
View File
@@ -13,7 +13,7 @@
.index-cards
- harvest.crop.plant_parts.order(:name).each do |plant_part|
.card
= link_to new_harvest_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}) do
= link_to harvests_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}), method: :post do
.card-title.text-center
%h3= plant_part_icon(plant_part.name)
%h3= plant_part.name
@@ -22,12 +22,10 @@
%h6 All Plant parts
%nav.nav
- PlantPart.all.order(:name).each do |plant_part|
= 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
= 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
= plant_part_icon(plant_part.name)
= plant_part
%a.btn#modalHarvestButton{"data-bs-target" => "#modelHarvestForm", "data-bs-toggle" => "modal", href: ""}
= harvest_icon
Record harvest
-1
View File
@@ -1,6 +1,5 @@
%h2= t('.recently_planted')
- 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
.d-flex.w-100.justify-content-between.homepage--list-item
%p.mb-2
+23
View File
@@ -0,0 +1,23 @@
- content_for :title, "Set your location"
%h1 Set your location
= form_for @member, url: update_location_member_path(@member), method: :put do |f|
.form-group
= f.label :location
= f.text_field :location, class: 'form-control'
.form-group
= f.label :latitude
= f.text_field :latitude, class: 'form-control', readonly: true
.form-group
= f.label :longitude
= f.text_field :longitude, class: 'form-control', readonly: true
#map.set-location-map
.form-group
%button#find-me.btn.btn-default Find my location
= f.submit 'Update location', class: 'btn btn-primary'
+3
View File
@@ -65,6 +65,9 @@
= link_to edit_member_registration_path, class: 'btn btn-block' do
= member_icon
= t('members.edit_profile')
= link_to set_location_member_path(@member), class: 'btn btn-block' do
= icon('fas', 'map-marker')
Set location
- if can?(:create, Notification) && current_member != @member
= link_to new_message_path(recipient_id: @member.id), class: 'btn btn-block' do
+1 -1
View File
@@ -3,4 +3,4 @@
%button.close{ type: "button", "data-bs-dismiss" => "alert" }
%span{ "aria-hidden" => true } &times;
%span.sr-only Close
= sanitize(content)
= content
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env ruby
exec "./bin/rails", "server", *ARGV
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env ruby
require "rubygems"
require "bundler/setup"
# explicit rubocop config increases performance slightly while avoiding config confusion.
ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
load Gem.bin_path("rubocop", "rubocop")
+1 -5
View File
@@ -14,11 +14,7 @@ Bundler.require(*Rails.groups)
module Growstuff
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 8.0
# Rails 7.1+ requires a coder for `serialize`.
# We set YAML as the default for compatibility with gems like Mexican Sofa.
config.active_record.default_column_serializer = YAML
config.load_defaults 7.0
I18n.config.enforce_available_locales = true
+4 -2
View File
@@ -32,7 +32,6 @@ Rails.application.routes.draw do
resources :gardens, concerns: :has_photos, param: :slug do
get 'timeline' => 'charts/gardens#timeline', constraints: { format: 'json' }
post 'fetch_wikidata' => 'gardens#fetch_wikidata', on: :member
resources :garden_collaborators
end
@@ -114,6 +113,10 @@ Rails.application.routes.draw do
resources :timeline
resources :members, param: :slug do
member do
get :set_location
put :update_location
end
resources :gardens
resources :seeds
resources :plantings
@@ -158,7 +161,6 @@ Rails.application.routes.draw do
namespace :api do
namespace :v1 do
jsonapi_resources :activities
get "crops/search", to: "crops#search"
jsonapi_resources :crops
jsonapi_resources :gardens
jsonapi_resources :harvests
@@ -1,7 +0,0 @@
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
+2 -5
View File
@@ -10,9 +10,9 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_04_29_132911) do
ActiveRecord::Schema[7.2].define(version: 2026_04_29_132911) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
enable_extension "plpgsql"
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
@@ -631,9 +631,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_04_29_132911) do
t.decimal "area"
t.string "area_unit"
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 ["owner_id"], name: "index_gardens_on_owner_id"
t.index ["slug"], name: "index_gardens_on_slug", unique: true
@@ -115,21 +115,6 @@ describe HarvestsController, :search do
it { expect(Harvest.last.planting.id).to eq(planting.id) }
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
describe "with invalid params" do
@@ -186,18 +171,6 @@ describe HarvestsController, :search do
it { expect(response).to redirect_to(harvest) }
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
describe "with invalid params" do
+13 -7
View File
@@ -2,14 +2,15 @@
require 'rails_helper'
describe PhotosController do
describe PhotosController, :search do
login_member
describe 'GET index' do
describe 'all photos' do
let!(:photo) { create(:photo) }
let!(:photo) { create(:photo, :reindex) }
before do
Photo.reindex
get :index
end
@@ -20,23 +21,28 @@ describe PhotosController do
end
describe '#index crop photos' do
let!(:photo) { create(:photo, owner: member, title: 'no assocations photo') }
let!(:crop_photo) { create(:photo, owner: member, title: 'photos of planting') }
let!(:crop) { create(:crop) }
let!(:planting) { create(:planting, crop:, owner: member) }
let!(:photo) { create(:photo, :reindex, owner: member, title: 'no assocations photo') }
let!(:crop_photo) { create(:photo, :reindex, owner: member, title: 'photos of planting') }
let!(:planting) { create(:planting, :reindex, crop:, owner: member) }
let!(:crop) { create(:crop, :reindex) }
before do
planting.photos << crop_photo
Photo.reindex
get :index, params: { crop_slug: crop.to_param }
end
describe "find photos by crop" do
it "has indexed the photos of this crop" do
expect(Photo.search).to include crop_photo
end
it "assigns crop" do
expect(assigns(:crop)).to eq crop
end
it { expect(assigns(:photos).size).to eq 1 }
it { expect(assigns(:photos).first.crops).to include crop }
it { expect(assigns(:photos).first.crops).to include crop.id }
it { expect(assigns(:photos).first.id).to eq crop_photo.id }
end
end
+5
View File
@@ -19,5 +19,10 @@ FactoryBot.define do
license_url { nil }
end
trait :reindex do
after(:create) do |photo, _evaluator|
photo.reindex(refresh: true)
end
end
end
end
@@ -54,8 +54,6 @@ describe "Harvesting a crop", :js, :search do
visit crop_path(maize)
click_link "Record harvest"
click_link plant_part.name
# We then navigate to the new_harvest_path, and save.
click_button "Save"
end
it { expect(page).to have_content "harvest was successfully created." }
@@ -71,22 +69,9 @@ describe "Harvesting a crop", :js, :search do
click_link plant_part.name
end
it "saves" do
# We then navigate to the new_harvest_path, and save.
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
it { expect(page).to have_content "harvest was successfully created." }
it { expect(page).to have_content planting.garden.name }
it { expect(page).to have_content "maize" }
end
context "Editing a harvest" do
+1
View File
@@ -28,6 +28,7 @@ describe "home page", :search do
Planting.reindex
Seed.reindex
Harvest.reindex
Photo.reindex
visit root_path
end
+5 -1
View File
@@ -6,10 +6,14 @@ describe 'Likeable', :js, :search do
let(:another_member) { create(:london_member) }
let!(:post) { create(:post, :reindex, author: member) }
let!(:activity) { create(:activity, :reindex, owner: member) }
let!(:photo) { create(:photo, owner: member) }
let!(:photo) { create(:photo, :reindex, owner: member) }
let!(:harvest) { create(:harvest, :reindex, owner: member) }
let!(:planting) { create(:planting, :reindex, owner: member) }
before do
Photo.reindex
end
include_context 'signed in member'
shared_examples 'object can be liked' do
@@ -0,0 +1,83 @@
require 'rails_helper'
RSpec.feature 'Set location', type: :feature do
let(:member) { FactoryBot.create(:member) }
before do
login_as(member, scope: :member)
end
scenario 'member sets their location by clicking on the map', js: true do
visit set_location_member_path(member)
# Test clicking on the map
page.execute_script("map.fire('click', { latlng: L.latLng(40.7128, -74.0060) })")
expect(find('#member_latitude').value).to eq('40.7128')
expect(find('#member_longitude').value).to eq('-74.006')
# Mock geocoding
geocoder_result = instance_double('Geocoder::Result::Nominatim',
city: 'New York',
town: nil,
village: nil,
hamlet: nil)
allow(Geocoder).to receive(:search).with([40.71, -74.01]).and_return([geocoder_result])
click_button 'Update location'
expect(page).to have_content('Location updated.')
member.reload
expect(member.location).to eq('New York')
expect(member.latitude).to eq(40.71)
expect(member.longitude).to eq(-74.01)
end
scenario 'member uses "Find my location"', js: true do
visit set_location_member_path(member)
# Mock browser's geolocation
page.execute_script("
navigator.geolocation.getCurrentPosition = function(success) {
var position = { coords: { latitude: 34.0522, longitude: -118.2437 } };
success(position);
}
")
click_button 'Find my location'
expect(find('#member_latitude').value).to eq('34.0522')
expect(find('#member_longitude').value).to eq('-118.2437')
# Mock geocoding
geocoder_result = instance_double('Geocoder::Result::Nominatim',
city: 'Los Angeles',
town: nil,
village: nil,
hamlet: nil)
allow(Geocoder).to receive(:search).with([34.05, -118.24]).and_return([geocoder_result])
click_button 'Update location'
expect(page).to have_content('Location updated.')
member.reload
expect(member.location).to eq('Los Angeles')
expect(member.latitude).to eq(34.05)
expect(member.longitude).to eq(-118.24)
end
scenario 'geocoding fails', js: true do
visit set_location_member_path(member)
page.execute_script("map.fire('click', { latlng: L.latLng(1.2345, 6.7890) })")
allow(Geocoder).to receive(:search).with([1.23, 6.79]).and_return([])
click_button 'Update location'
expect(page).to have_content('Location updated.')
member.reload
expect(member.location).to eq('Location near 1.23, 6.79')
expect(member.latitude).to eq(1.23)
expect(member.longitude).to eq(6.79)
end
end
@@ -1,32 +0,0 @@
# 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
+33 -1
View File
@@ -3,7 +3,7 @@
require 'rails_helper'
describe Photo do
let(:photo) { create(:photo, owner: member) }
let(:photo) { create(:photo, :reindex, owner: member) }
let(:member) { create(:member) }
it_behaves_like "it is likeable"
@@ -237,4 +237,36 @@ describe Photo do
end
end
describe 'Elastic search indexing', :search do
let!(:planting) { create(:planting, :reindex, owner: photo.owner) }
let!(:crop) { create(:crop, :reindex) }
before do
planting.photos << photo
described_class.reindex
described_class.searchkick_index.refresh
end
describe "finds all photos in search index" do
it "finds just one" do
expect(described_class.search.count).to eq 1
end
it "finds the matching photo" do
expect(described_class.search).to include photo
end
it "retrieves crops from ES" do
expect(described_class.search(load: false).first.crops).to eq [planting.crop.id]
end
end
it "finds photos by owner in search index" do
expect(described_class.search(where: { owner_id: planting.owner_id })).to include photo
end
it "finds photos by crop in search index" do
expect(described_class.search(where: { crops: planting.crop.id })).to include photo
end
end
end
-35
View File
@@ -1,35 +0,0 @@
# 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
+1
View File
@@ -47,6 +47,7 @@ RSpec.configure do |config|
# reindex models
Crop.reindex
Harvest.reindex
Photo.reindex
Planting.reindex
Seed.reindex
end
-79
View File
@@ -9,85 +9,6 @@
"/crops": {
"get": {
"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": [
"crops"
],
+12 -12
View File
@@ -735,9 +735,9 @@ fast-levenshtein@^2.0.6:
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fast-uri@^3.0.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec"
integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==
version "3.1.0"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
fastq@^1.6.0:
version "1.20.1"
@@ -915,9 +915,9 @@ inherits@2, inherits@~2.0.3:
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ip-address@^10.0.1:
version "10.2.0"
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.2.0.tgz#805fc178b20c518bd4c8548b24fe30892d7f3206"
integrity sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==
version "10.1.0"
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4"
integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==
is-arrayish@^0.2.1:
version "0.2.1"
@@ -964,9 +964,9 @@ js-tokens@^4.0.0:
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^4.1.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.2.0.tgz#2bd9e85682dd91bd469afb809d816043b3d49524"
integrity sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies:
argparse "^2.0.1"
@@ -1396,9 +1396,9 @@ supports-preserve-symlinks-flag@^1.0.0:
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
systeminformation@^5.25.11:
version "5.31.6"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.31.6.tgz#2da4979a7262974fd068a3a306ded30aed6127c0"
integrity sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA==
version "5.31.5"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.31.5.tgz#e839fa6b40620a8bee010eb9d9d55c2d5f7042c8"
integrity sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==
to-regex-range@^5.0.1:
version "5.0.1"