diff --git a/.travis.yml b/.travis.yml index 63706e849..f9aa1768d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index d96c7f266..f36a94e39 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -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 diff --git a/app/models/concerns/predict_harvest.rb b/app/models/concerns/predict_harvest.rb index bc377f369..51224a2a6 100644 --- a/app/models/concerns/predict_harvest.rb +++ b/app/models/concerns/predict_harvest.rb @@ -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 diff --git a/app/models/garden.rb b/app/models/garden.rb index b30418486..7aee416fc 100644 --- a/app/models/garden.rb +++ b/app/models/garden.rb @@ -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", diff --git a/app/models/planting.rb b/app/models/planting.rb index 686153b64..5539b6268 100644 --- a/app/models/planting.rb +++ b/app/models/planting.rb @@ -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 diff --git a/app/views/plantings/_owner.haml b/app/views/plantings/_owner.haml index 89bfcae4d..3b0b931d8 100644 --- a/app/views/plantings/_owner.haml +++ b/app/views/plantings/_owner.haml @@ -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 \ No newline at end of file diff --git a/app/views/plantings/_timeline.html.haml b/app/views/plantings/_timeline.html.haml index 925456c9b..ee317f299 100644 --- a/app/views/plantings/_timeline.html.haml +++ b/app/views/plantings/_timeline.html.haml @@ -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)} diff --git a/app/views/plantings/show.html.haml b/app/views/plantings/show.html.haml index 0ff106ccc..4be8a3f9e 100644 --- a/app/views/plantings/show.html.haml +++ b/app/views/plantings/show.html.haml @@ -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 diff --git a/script/install_codeclimate.sh b/script/install_codeclimate.sh deleted file mode 100755 index f48091a9a..000000000 --- a/script/install_codeclimate.sh +++ /dev/null @@ -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 diff --git a/spec/factories/member.rb b/spec/factories/member.rb index 37ed2c185..87b5326f3 100644 --- a/spec/factories/member.rb +++ b/spec/factories/member.rb @@ -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 diff --git a/spec/features/plantings/prediction_spec.rb b/spec/features/plantings/prediction_spec.rb new file mode 100644 index 000000000..b775fd8fd --- /dev/null +++ b/spec/features/plantings/prediction_spec.rb @@ -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 diff --git a/spec/features/plantings/show_spec.rb b/spec/features/plantings/show_spec.rb index 9d8450834..f67abc6e2 100644 --- a/spec/features/plantings/show_spec.rb +++ b/spec/features/plantings/show_spec.rb @@ -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) } diff --git a/spec/models/planting_spec.rb b/spec/models/planting_spec.rb index 3bbd0a255..565fb5801 100644 --- a/spec/models/planting_spec.rb +++ b/spec/models/planting_spec.rb @@ -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 diff --git a/spec/views/plantings/show.html.haml_spec.rb b/spec/views/plantings/show.html.haml_spec.rb index efb3482ed..c774c5d48 100644 --- a/spec/views/plantings/show.html.haml_spec.rb +++ b/spec/views/plantings/show.html.haml_spec.rb @@ -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