Merge pull request #2286 from Br3nda/predict-perenial-harvest

Predict month of harvest on perennials [EDIT: show nearby harvesting months for all crops]
This commit is contained in:
Brenda Wallace
2019-11-25 14:56:13 +13:00
committed by GitHub
14 changed files with 227 additions and 52 deletions

View File

@@ -29,14 +29,8 @@ before_install:
# - sudo apt update
- sudo apt install dpkg
- sudo apt install google-chrome-stable
- ./script/install_codeclimate.sh
- ./script/install_linters.sh
- ./script/install_elasticsearch.sh
before_script:
- >
if [ "${COVERAGE}" = "true" ]; then
./cc-test-reporter before-build
fi
script:
- set -e
- >
@@ -52,7 +46,8 @@ script:
after_script:
- >
if [ "${COVERAGE}" = "true" ]; then
./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT;
gem install codeclimate-test-reporter
codeclimate-test-reporter
fi
- >
if [ "${PERCY}" = "true" ]; then

View File

@@ -33,6 +33,9 @@ class PlantingsController < ApplicationController
def show
@photos = @planting.photos.includes(:owner).order(date_taken: :desc)
@matching_seeds = matching_seeds
@neighbours = @planting.nearby_same_crop
.where.not(id: @planting.id)
.limit(6)
respond_with @planting
end

View File

@@ -54,6 +54,27 @@ module PredictHarvest
first_harvest_predicted_at > Time.zone.today
end
def harvest_months
Rails.cache.fetch("#{cache_key_with_version}/harvest_months", expires_in: 5.minutes) do
neighbours_for_harvest_predictions.where.not(harvested_at: nil)
.group("extract(MONTH from harvested_at)::int")
.count
end
end
def neighbours_for_harvest_predictions
# use this planting's harvest if any
return harvests if harvests.size.positive?
# otherwise use nearby plantings
if location
return Harvest.where(planting: nearby_same_crop.has_harvests)
.where.not(planting_id: nil)
end
Harvest.none
end
private
def harvests_with_dates

View File

@@ -34,6 +34,11 @@ class Garden < ApplicationRecord
numericality: { only_integer: false, greater_than_or_equal_to: 0 },
allow_nil: true
scope :located, lambda {
where.not(gardens: { location: '' })
.where.not(gardens: { latitude: nil })
.where.not(gardens: { longitude: nil })
}
AREA_UNITS_VALUES = {
"square metres" => "square metre",
"square feet" => "square foot",

View File

@@ -32,11 +32,18 @@ class Planting < ApplicationRecord
##
## Scopes
scope :located, lambda {
joins(:garden)
.where.not(gardens: { location: '' })
.where.not(gardens: { latitude: nil })
.where.not(gardens: { longitude: nil })
}
scope :active, -> { where('finished <> true').where('finished_at IS NULL OR finished_at < ?', Time.zone.now) }
scope :annual, -> { joins(:crop).where(crops: { perennial: false }) }
scope :perennial, -> { joins(:crop).where(crops: { perennial: true }) }
scope :interesting, -> { has_photos.one_per_owner.order(planted_at: :desc) }
scope :recent, -> { order(created_at: :desc) }
scope :has_harvests, -> { where('plantings.harvests_count > 0') }
scope :one_per_owner, lambda {
joins("JOIN members m ON (m.id=plantings.owner_id)
LEFT OUTER JOIN plantings p2
@@ -48,7 +55,7 @@ class Planting < ApplicationRecord
delegate :name, :slug, :en_wikipedia_url, :default_scientific_name, :plantings_count,
to: :crop, prefix: true
delegate :annual?, :svg_icon, to: :crop
delegate :annual?, :perennial?, :svg_icon, to: :crop
delegate :location, :longitude, :latitude, to: :garden
##
@@ -96,6 +103,17 @@ class Planting < ApplicationRecord
planted? && !finished?
end
def nearby_same_crop
return Planting.none if location.blank?
# latitude, longitude = Geocoder.coordinates(location, params: { limit: 1 })
Planting.joins(:garden)
.where(crop: crop)
.located
.where('gardens.latitude < ? AND gardens.latitude > ?',
latitude + 10, latitude - 10)
end
private
# check that any finished_at date occurs after planted_at

View File

@@ -10,9 +10,9 @@
%p
Planted in
= link_to @planting.garden, @planting.garden
- if @planting.owner.location
- if @planting.garden.location
%p
%small
View other plantings, members and more near
= link_to @planting.owner.location, place_path(@planting.owner.location, anchor: "plantings")
= link_to @planting.garden.location, place_path(@planting.garden.location, anchor: "plantings")
.col= render "members/avatar", member: @planting.owner

View File

@@ -1,23 +1,39 @@
- if @planting.crop.annual?
- if planting.annual?
.d-flex.justify-content-between
- if @planting.planted_at.present?
%p.small #{ image_icon 'planting-hand'} Planted #{I18n.l @planting.planted_at}
- if @planting.first_harvest_date.present?
%p.small #{harvest_icon} Harvest started #{I18n.l @planting.first_harvest_date}
- elsif @planting.first_harvest_predicted_at.present?
%p.small #{harvest_icon} First harvest expected #{I18n.l @planting.first_harvest_predicted_at}
- if @planting.finished_at.present?
%p.small #{finished_icon} Finished #{I18n.l @planting.finished_at}
- elsif @planting.finish_predicted_at.present?
%p.small #{finished_icon} Finish expected #{I18n.l @planting.finish_predicted_at}
- if @planting.planted_at.present? && @planting.expected_lifespan.present?
- if planting.planted_at.present?
%p.small #{ image_icon 'planting-hand'} Planted #{I18n.l planting.planted_at}
- if planting.first_harvest_date.present?
%p.small #{harvest_icon} Harvest started #{I18n.l planting.first_harvest_date}
- elsif planting.first_harvest_predicted_at.present?
%p.small #{harvest_icon} First harvest expected #{I18n.l planting.first_harvest_predicted_at}
- if planting.finished_at.present?
%p.small #{finished_icon} Finished #{I18n.l planting.finished_at}
- elsif planting.finish_predicted_at.present?
%p.small #{finished_icon} Finish expected #{I18n.l planting.finish_predicted_at}
- if planting.planted_at.present? && planting.expected_lifespan.present?
.progress
.progress-bar{"aria-valuemax" => "100", "aria-valuemin" => "0", "aria-valuenow" => @planting.percentage_grown, role: "progressbar", style: "width: #{@planting.percentage_grown}%"}
.progress-bar{"aria-valuemax" => "100", "aria-valuemin" => "0", "aria-valuenow" => planting.percentage_grown, role: "progressbar", style: "width: #{planting.percentage_grown}%"}
%ul.list-unstyled.d-flex.justify-content-between
- in_weeks(@planting.expected_lifespan).times do |week_number|
%li{class: @planting.planted_at + week_number.weeks > Time.zone.today ? 'text-muted progress-fade' : '', 'data-toggle': "tooltip", 'data-placement': "top", title: I18n.l(@planting.planted_at + week_number.weeks)}
- in_weeks(planting.expected_lifespan).times do |week_number|
%li{class: planting.planted_at + week_number.weeks > Time.zone.today ? 'text-muted progress-fade' : '', 'data-toggle': "tooltip", 'data-placement': "top", title: I18n.l(planting.planted_at + week_number.weeks)}
= render 'timeline_icon',
planting: @planting,
planting: planting,
week_number: week_number,
date_this_week: @planting.planted_at + week_number.weeks
(One emojii = 1 week)
date_this_week: planting.planted_at + week_number.weeks
%small (One emojii = 1 week)
.harvest-months
Harvest months:
- if planting.harvest_months.empty?
%span.text-muted
We need more data on this crop in your latitude. There's not enough
info yet to predict harvests.
- else
- (1..12).each do |month|
- if planting.harvest_months.keys().include?(month.to_f)
.badge.badge-info.badge-harvesting{id: "month-#{month}"}
= I18n.t('date.abbr_month_names')[month]
- else
.badge.text-muted{'aria-hidden': "true", id: "month-#{month}"}
= I18n.t('date.abbr_month_names')[month]
- unless planting.garden.location.blank?
in #{link_to planting.garden.location, place_path(planting.garden.location)}

View File

@@ -14,6 +14,15 @@
%li.breadcrumb-item= link_to @planting.owner, member_plantings_path(@planting.owner)
%li.breadcrumb-item.active= link_to @planting.crop.name, @planting
- if @planting.parent_seed.nil? && @matching_seeds && @matching_seeds.any? && @planting.owner == current_member
.alert.alert-info{role: "alert"}
= bootstrap_form_for(@planting) do |f|
Is this from one of these plantings?
- @matching_seeds.each do |seed|
= f.radio_button :parent_seed_id, seed.id, label: seed
= f.submit "save", class: 'btn btn-sm'
.planting
.row
.col-md-8.col-xs-12
@@ -30,13 +39,6 @@
= render 'timeline', planting: @planting
- if @planting.parent_seed.nil? && @matching_seeds && @matching_seeds.any? && @planting.owner == current_member
.alert.alert-info{role: "alert"}
= bootstrap_form_for(@planting) do |f|
Is this from one of these plantings?
- @matching_seeds.each do |seed|
= f.radio_button :parent_seed_id, seed.id, label: seed
= f.submit "save", class: 'btn btn-sm'
.col-md-4.col-xs-12
= render 'plantings/owner', planting: @planting
@@ -52,16 +54,33 @@
:growstuff_markdown
#{strip_tags(@planting.description)}
%section= render 'plantings/photos', photos: @photos, planting: @planting
%section.harvests
%a{name: 'harvests'}
= render 'plantings/harvests', planting: @planting
%section.descendants
%a{name: 'seeds'}
= render 'plantings/descendants', planting: @planting
.col-md-4.col-xs-12
= render 'plantings/actions', planting: @planting
%hr/
= render @planting.crop
.row
.col-md-6
%section.harvests
%a{name: 'harvests'}
= render 'plantings/harvests', planting: @planting
.col-md-6
%section.descendants
%a{name: 'seeds'}
= render 'plantings/descendants', planting: @planting
- if @planting.location
%section.neighbours
%h2 World Neighbours
- @neighbours.each do |planting|
= link_to planting, class: 'list-group-item list-group-item-action flex-column align-items-start' do
.d-flex.w-100.justify-content-between
%p.mb-2
= image_tag planting_image_path(planting), width: 75, class: 'rounded shadow'
.text-right
%h5= planting.crop.name
- if planting.planted_from.present?
%span.badge.badge-success= planting.planted_from.pluralize
%small.text-muted
planted by #{planting.owner}
in #{planting.garden.location}
%p Other #{@planting.crop.name} plantings at the same latitude

View File

@@ -1,7 +0,0 @@
#!/bin/bash
if [ "${COVERAGE}" = "true" ]; then
set -euv
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter;
chmod +x ./cc-test-reporter;
fi

View File

@@ -39,7 +39,7 @@ FactoryBot.define do
factory :london_member do
sequence(:login_name) { |n| "JohnH#{n}" } # for the astronomer who figured out longitude
location { 'Greenwich, UK' }
# including lat/long explicitly because geocoder doesn't work with FG
# including lat/long explicitly because geocoder doesn't work with FactoryBot
latitude { 51.483 }
longitude { 0.004 }
end

View File

@@ -0,0 +1,60 @@
require "rails_helper"
require 'custom_matchers'
describe "Display a planting", :js, :elasticsearch do
describe 'planting perennial' do
let(:garden) { FactoryBot.create :garden, location: 'Edinburgh' }
let(:crop) { FactoryBot.create(:crop, name: 'feijoa', perennial: true) }
let(:planting) { FactoryBot.create :planting, crop: crop, garden: garden, owner: garden.owner }
describe 'no harvest to predict from' do
before { visit planting_path(planting) }
it { expect(planting.harvest_months).to eq({}) }
it { expect(page).to have_content 'We need more data on this crop in your latitude.' }
end
describe 'harvests used to predict' do
before do
FactoryBot.create :harvest, planting: planting, crop: crop, harvested_at: '1 May 2019'
FactoryBot.create :harvest, planting: planting, crop: crop, harvested_at: '18 June 2019'
FactoryBot.create_list :harvest, 4, planting: planting, crop: crop, harvested_at: '18 August 2019'
end
before { visit planting_path(planting) }
it { expect(page.find("#month-1")[:class]).not_to include("badge-harvesting") }
it { expect(page.find("#month-2")[:class]).not_to include("badge-harvesting") }
it { expect(page.find("#month-5")[:class]).to include("badge-harvesting") }
it { expect(page.find("#month-6")[:class]).to include("badge-harvesting") }
it { expect(page.find("#month-8")[:class]).to include("badge-harvesting") }
end
describe 'nearby plantings used to predict' do
# Note the locations used need to be stubbed in geocoder
before do
# Near by planting with harvests
nearby_garden = FactoryBot.create :garden, location: 'Greenwich, UK'
nearby_planting = FactoryBot.create :planting, crop: crop,
garden: nearby_garden, owner: nearby_garden.owner, planted_at: '1 January 2000'
FactoryBot.create :harvest, planting: nearby_planting, crop: crop,
harvested_at: '1 May 2019'
FactoryBot.create :harvest, planting: nearby_planting, crop: crop,
harvested_at: '18 June 2019'
FactoryBot.create_list :harvest, 4, planting: nearby_planting, crop: crop,
harvested_at: '18 August 2008'
# far away planting harvests
faraway_garden = FactoryBot.create :garden, location: 'Amundsen-Scott Base, Antarctica'
faraway_planting = FactoryBot.create :planting, garden: faraway_garden, crop: crop,
owner: faraway_garden.owner, planted_at: '16 May 2001'
FactoryBot.create_list :harvest, 4, planting: faraway_planting, crop: crop,
harvested_at: '18 December 2006'
end
before { visit planting_path(planting) }
it { expect(page.find("#month-1")[:class]).not_to include("badge-harvesting") }
it { expect(page.find("#month-2")[:class]).not_to include("badge-harvesting") }
it { expect(page.find("#month-5")[:class]).to include("badge-harvesting") }
it { expect(page.find("#month-6")[:class]).to include("badge-harvesting") }
it { expect(page.find("#month-8")[:class]).to include("badge-harvesting") }
end
end
end

View File

@@ -61,6 +61,7 @@ describe "Display a planting", :js, :elasticsearch do
it { expect(find('.plantingfact--quantity')).to have_text '100' }
end
end
context 'signed in' do
include_context 'signed in member'
before { visit planting_path(planting) }

View File

@@ -207,6 +207,49 @@ describe Planting do
end
end
describe 'planting perennial' do
let(:crop) { FactoryBot.create(:crop, name: 'feijoa', perennial: true) }
it { expect(planting.perennial?).to eq true }
describe 'no harvest to predict from' do
it { expect(planting.harvest_months).to eq({}) }
end
describe 'harvests used to predict' do
before do
FactoryBot.create :harvest, planting: planting, crop: crop, harvested_at: '1 May 2019'
FactoryBot.create :harvest, planting: planting, crop: crop, harvested_at: '18 June 2019'
FactoryBot.create_list :harvest, 4, planting: planting, crop: crop, harvested_at: '18 August 2019'
end
it { expect(planting.harvest_months).to eq(5 => 1, 6 => 1, 8 => 4) }
end
describe 'nearby plantings used to predict' do
# Note the locations used need to be stubbed in geocoder
let(:garden) { FactoryBot.create :garden, location: 'Edinburgh', owner: garden_owner }
before do
# Near by planting with harvests
nearby_garden = FactoryBot.create :garden, location: 'Greenwich, UK'
nearby_planting = FactoryBot.create :planting, crop: crop,
garden: nearby_garden, owner: nearby_garden.owner, planted_at: '1 January 2000'
FactoryBot.create :harvest, planting: nearby_planting, crop: crop,
harvested_at: '1 May 2019'
FactoryBot.create :harvest, planting: nearby_planting, crop: crop,
harvested_at: '18 June 2019'
FactoryBot.create_list :harvest, 4, planting: nearby_planting, crop: crop,
harvested_at: '18 August 2008'
# far away planting harvests
faraway_garden = FactoryBot.create :garden, location: 'Amundsen-Scott Base, Antarctica'
faraway_planting = FactoryBot.create :planting, garden: faraway_garden, crop: crop,
owner: faraway_garden.owner, planted_at: '16 May 2001'
FactoryBot.create_list :harvest, 4, planting: faraway_planting, crop: crop,
harvested_at: '18 December 2006'
end
it { expect(planting.harvest_months).to eq(5 => 1, 6 => 1, 8 => 4) }
end
end
it 'has an owner' do
planting.owner.should be_an_instance_of Member
end

View File

@@ -13,6 +13,7 @@ describe "plantings/show" do
before do
assign(:planting, planting)
assign(:photos, planting.photos.paginate(page: 1))
assign(:neighbours, planting.nearby_same_crop)
controller.stub(:current_user) { member }
end