diff --git a/Gemfile b/Gemfile index bc3d45a34..c55d5e70f 100644 --- a/Gemfile +++ b/Gemfile @@ -87,8 +87,8 @@ gem "chartkick" # elasticsearch we use # See https://github.com/elastic/elasticsearch-ruby#compatibility gem "elasticsearch-api", "~> 2.0.0" -gem "elasticsearch-model" -gem "elasticsearch-rails" +gem "elasticsearch-model", ">= 5.0.0" +gem "elasticsearch-rails", ">= 5.0.0" gem "hashie", ">= 3.5.3" gem 'rake', '>= 10.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index e19af5749..8c340a749 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -51,7 +51,7 @@ GEM public_suffix (>= 2.0.2, < 4.0) arel (6.0.4) ast (2.4.0) - autoprefixer-rails (8.3.0) + autoprefixer-rails (8.4.1) execjs bcrypt (3.1.11) better_errors (2.2.0) @@ -179,7 +179,7 @@ GEM font-awesome-sass (5.0.9) sass (>= 3.2) formatador (0.2.5) - friendly_id (5.2.3) + friendly_id (5.2.4) activerecord (>= 4.0.0) geocoder (1.4.7) gibbon (1.2.1) @@ -367,7 +367,7 @@ GEM public_suffix (3.0.2) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) - rack (1.6.9) + rack (1.6.10) rack-protection (2.0.1) rack rack-test (0.6.3) @@ -507,7 +507,7 @@ GEM trollop (1.16.2) tzinfo (1.2.5) thread_safe (~> 0.1) - uglifier (4.1.9) + uglifier (4.1.10) execjs (>= 0.3.0, < 3) unicode-display_width (1.3.2) unicorn (5.4.0) @@ -559,8 +559,8 @@ DEPENDENCIES database_cleaner devise elasticsearch-api (~> 2.0.0) - elasticsearch-model - elasticsearch-rails + elasticsearch-model (>= 5.0.0) + elasticsearch-rails (>= 5.0.0) factory_bot_rails faker figaro @@ -619,6 +619,7 @@ DEPENDENCIES will_paginate xmlrpc + RUBY VERSION ruby 2.4.1p111 diff --git a/app/assets/stylesheets/custom_bootstrap/variables.sass b/app/assets/stylesheets/custom_bootstrap/variables.sass index f94f04106..f2fc82dda 100644 --- a/app/assets/stylesheets/custom_bootstrap/variables.sass +++ b/app/assets/stylesheets/custom_bootstrap/variables.sass @@ -1,7 +1,7 @@ // Use this file to override Twitter Bootstrap variables or define own variables. // Import original variables so they can be used in overrides -//@import 'bootstrap/variables.scss' +@import 'bootstrap/variables.scss' // Base colours @@ -10,7 +10,7 @@ $brown: #413f3b $green: #5f8e43 $blue: #2f4365 -$red: #8e4d43 +$red: #ff4d43 $orange: #ffa500 $yellow: #b2935c $white: #ffffff diff --git a/app/assets/stylesheets/overrides.sass b/app/assets/stylesheets/overrides.sass index f61ec813a..b255536ea 100644 --- a/app/assets/stylesheets/overrides.sass +++ b/app/assets/stylesheets/overrides.sass @@ -12,6 +12,10 @@ body .list-inline > li.first padding-left: 0px +.activity-list + list-style-type: none + padding: 0 + h2 font-size: 150% @@ -34,6 +38,21 @@ h3 max-width: 100% height: auto +.profile-sidebar + margin-top: -5rem + .avatar + border-radius: 50% + border-radius: 50% + z-index: 2 + position: relative + +.profile-activity + background: white + padding: 2em + margin-top: 2em + .container + width: 100% + .sidebar border-left: 1px solid darken($beige, 10%) margin-left: -1px @@ -93,7 +112,32 @@ p.stats padding-left: 1em width: 15em +.progress + border-radius: 0 + +.badge-super-late + background-color: $red +.badge-harvest + background-color: $blue + +.planting-super-late +.planting-late + background-color: $beige + .planting + .planting-badges + position: absolute + + .planting-thumbnail + padding: 0 + border: 1px solid darken($beige, 10%) + border-radius: 4px + .planting-actions + top: -8em + .planting-name + position: relative + top: -1em + dl.planting-attributes dt text-align: left @@ -108,6 +152,7 @@ p.stats #membermap height: 250px + z-index: 0 .location-not-set height: 250px @@ -155,6 +200,7 @@ p.stats text-align: center margin-bottom: 1.5em max-width: 160px + max-height: 200px .member-thumbnail text-align: left diff --git a/app/controllers/harvests_controller.rb b/app/controllers/harvests_controller.rb index 2e7fbef3e..73aaafd86 100644 --- a/app/controllers/harvests_controller.rb +++ b/app/controllers/harvests_controller.rb @@ -92,7 +92,7 @@ class HarvestsController < ApplicationController # if this harvest is not linked to a planting, then do nothing return if @harvest.planting.nil? - @harvest.planting.update_harvest_days + @harvest.planting.update_harvest_days! @harvest.crop.update_harvest_medians end end diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index c346eb8e3..ae720e57b 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -20,6 +20,7 @@ class MembersController < ApplicationController @facebook_auth = @member.auth('facebook') @posts = @member.posts @gardens = @member.gardens.active.order(:name) + @harvests = @member.harvests # The garden form partial is called from the "New Garden" tab; # it requires a garden to be passed in @garden. diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index f6da9d076..5ee31ea2c 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -77,7 +77,7 @@ class PlantingsController < ApplicationController end def update_planting_medians - @planting.update_harvest_days + @planting.update_harvest_days! end def planting_params diff --git a/app/helpers/plantings_helper.rb b/app/helpers/plantings_helper.rb index f71dc8158..4ea42f43e 100644 --- a/app/helpers/plantings_helper.rb +++ b/app/helpers/plantings_helper.rb @@ -32,4 +32,24 @@ module PlantingsHelper def plantings_active_tickbox_path(owner, show_all) show_inactive_tickbox_path('plantings', owner, show_all) end + + def days_from_now_to_finished(planting) + return unless planting.finish_is_predicatable? + (planting.finish_predicted_at - Time.zone.today).to_i + end + + def days_from_now_to_first_harvest(planting) + return unless planting.planted_at.present? && planting.first_harvest_predicted_at.present? + (planting.first_harvest_predicted_at - Time.zone.today).to_i + end + + def planting_classes(planting) + classes = [] + classes << 'planting-growing' if planting.growing? + classes << 'planting-finished' if planting.finished? + classes << 'planting-harvest-time' if planting.harvest_time? + classes << 'planting-late' if planting.late? + classes << 'planting-super-late' if planting.super_late? + classes.join(' ') + end end diff --git a/app/models/concerns/predict_harvest.rb b/app/models/concerns/predict_harvest.rb new file mode 100644 index 000000000..d5422ee35 --- /dev/null +++ b/app/models/concerns/predict_harvest.rb @@ -0,0 +1,61 @@ +module PredictHarvest + extend ActiveSupport::Concern + + included do # rubocop:disable Metrics/BlockLength + # dates + def first_harvest_date + harvests_with_dates.minimum(:harvested_at) + end + + def last_harvest_date + harvests_with_dates.maximum(:harvested_at) + end + + def first_harvest_predicted_at + return unless crop.median_days_to_first_harvest.present? && planted_at.present? + planted_at + crop.median_days_to_first_harvest.days + end + + def last_harvest_predicted_at + return unless crop.median_days_to_last_harvest.present? && planted_at.present? + planted_at + crop.median_days_to_last_harvest.days + end + + # actions + def update_harvest_days! + days_to_first_harvest = nil + days_to_last_harvest = nil + if planted_at.present? && harvests_with_dates.size.positive? + days_to_first_harvest = (first_harvest_date - planted_at).to_i + days_to_last_harvest = (last_harvest_date - planted_at).to_i if finished? + end + update(days_to_first_harvest: days_to_first_harvest, days_to_last_harvest: days_to_last_harvest) + end + + # status + def harvest_time? + return false if crop.perennial || finished + + # We have harvests but haven't finished + harvests.size.positive? || + + # or, we don't have harvests, but we predict we should by now + (first_harvest_predicted_at.present? && + harvests.empty? && + first_harvest_predicted_at < Time.zone.today) + end + + def before_harvest_time? + first_harvest_predicted_at.present? && + harvests.empty? && + first_harvest_predicted_at.present? && + first_harvest_predicted_at > Time.zone.today + end + + private + + def harvests_with_dates + harvests.where.not(harvested_at: nil) + end + end +end diff --git a/app/models/concerns/predict_planting.rb b/app/models/concerns/predict_planting.rb new file mode 100644 index 000000000..ff66168d8 --- /dev/null +++ b/app/models/concerns/predict_planting.rb @@ -0,0 +1,80 @@ +module PredictPlanting + extend ActiveSupport::Concern + + included do # rubocop:disable Metrics/BlockLength + ## Triggers + before_save :calculate_lifespan + + def calculate_lifespan + self.lifespan = (planted_at.present? && finished_at.present? ? finished_at - planted_at : nil) + end + + # dates + def finish_predicted_at + if planted_at.blank? + nil + elsif crop.median_lifespan.present? + planted_at + crop.median_lifespan.days + elsif crop.parent.present? && crop.parent.median_lifespan.present? + planted_at + crop.parent.median_lifespan.days + end + end + + # days + def expected_lifespan + if actual_lifespan.present? + actual_lifespan + elsif crop.median_lifespan.present? + crop.median_lifespan + elsif crop.parent.present? && crop.parent.median_lifespan.present? + crop.parent.median_lifespan + end + end + + def actual_lifespan + return unless planted_at.present? && finished_at.present? + (finished_at - planted_at).to_i + end + + def days_since_planted + (Time.zone.today - planted_at).to_i if planted_at.present? + end + + # progress + def percentage_grown + if finished? + 100 + elsif !finish_is_predicatable? + nil + elsif growing? + calculate_percentage_grown + elsif planted? + 0 + end + end + + # states + def finish_is_predicatable? + crop.annual? && planted_at.present? && finish_predicted_at.present? + end + + # Planting has live more then 90 days past predicted finish + def super_late? + late? && (finish_predicted_at + 90.days) < Time.zone.today + end + + def late? + crop.annual? && !finished && + planted_at.present? && + finish_predicted_at.present? && + finish_predicted_at <= Time.zone.today + end + + private + + def calculate_percentage_grown + percent = (days_since_planted / expected_lifespan.to_f) * 100 + (percent > 100 ? 100 : percent) + end + end +end diff --git a/app/models/crop.rb b/app/models/crop.rb index bead8e89b..b1d492353 100644 --- a/app/models/crop.rb +++ b/app/models/crop.rb @@ -185,7 +185,7 @@ class Crop < ActiveRecord::Base end def update_medians - plantings.each(&:update_harvest_days) + plantings.each(&:update_harvest_days!) update_lifespan_medians update_harvest_medians end diff --git a/app/models/planting.rb b/app/models/planting.rb index 3dff4c006..bea6b9a8b 100644 --- a/app/models/planting.rb +++ b/app/models/planting.rb @@ -3,6 +3,8 @@ class Planting < ActiveRecord::Base include PhotoCapable include Finishable include Ownable + include PredictPlanting + include PredictHarvest friendly_id :planting_slug, use: %i(slugged finders) # Constants @@ -13,10 +15,6 @@ class Planting < ActiveRecord::Base 'graft', 'layering' ].freeze - ## - ## Triggers - before_save :calculate_lifespan - belongs_to :garden belongs_to :crop, counter_cache: true has_many :harvests, dependent: :destroy @@ -59,6 +57,10 @@ class Planting < ActiveRecord::Base in: PLANTED_FROM_VALUES, message: "%s is not a valid planting method" } + def age_in_days + (Time.zone.today - planted_at).to_i if planted_at.present? + end + def planting_slug [ owner.login_name, @@ -81,61 +83,20 @@ class Planting < ActiveRecord::Base photos.order(created_at: :desc).first end + def finished? + finished || (finished_at.present? && finished_at <= Time.zone.today) + end + def planted? - planted_at.present? && planted_at <= Date.current + planted_at.present? && planted_at <= Time.zone.today end - def finish_predicted_at - planted_at + crop.median_lifespan.days if crop.median_lifespan.present? && planted_at.present? - end - - def calculate_lifespan - self.lifespan = (planted_at.present? && finished_at.present? ? finished_at - planted_at : nil) - end - - def expected_lifespan - if planted_at.present? && finished_at.present? - return (finished_at - planted_at).to_i - end - crop.median_lifespan - end - - def days_since_planted - (Time.zone.today - planted_at).to_i if planted_at.present? - end - - def percentage_grown - return 100 if finished - return if planted_at.blank? || expected_lifespan.blank? - p = (days_since_planted / expected_lifespan.to_f) * 100 - return p if p <= 100 - 100 - end - - def update_harvest_days - days_to_first_harvest = nil - days_to_last_harvest = nil - if planted_at.present? && harvests_with_dates.size.positive? - days_to_first_harvest = (first_harvest_date - planted_at).to_i - days_to_last_harvest = (last_harvest_date - planted_at).to_i if finished? - end - update(days_to_first_harvest: days_to_first_harvest, days_to_last_harvest: days_to_last_harvest) - end - - def first_harvest_date - harvests_with_dates.minimum(:harvested_at) - end - - def last_harvest_date - harvests_with_dates.maximum(:harvested_at) + def growing? + planted? && !finished? end private - def harvests_with_dates - harvests.where.not(harvested_at: nil) - end - # check that any finished_at date occurs after planted_at def finished_must_be_after_planted return unless planted_at && finished_at # only check if we have both diff --git a/app/views/gardens/_overview.html.haml b/app/views/gardens/_overview.html.haml index b8f9ce54a..ed12d0cf5 100644 --- a/app/views/gardens/_overview.html.haml +++ b/app/views/gardens/_overview.html.haml @@ -13,12 +13,9 @@ .col-md-10 .row - if garden.plantings.current.size.positive? - - garden.plantings.current.includes(:crop).each do |planting| - .col-md-2.col-sm-6.col-xs-6 - .hover-wrapper - .text - = render 'plantings/actions', planting: planting - = render partial: "plantings/thumbnail", locals: { planting: planting } + - garden.plantings.current.order(created_at: :desc).includes(:crop, :photos).each do |planting| + .col-md-2.col-sm-4.col-xs-6 + = render "plantings/thumbnail", planting: planting - else .col-md-2.col-sm-6.col-xs-6 no plantings - if can?(:edit, garden) diff --git a/app/views/harvests/_card.html.haml b/app/views/harvests/_card.html.haml index 52cfb3627..02ad5d3e0 100644 --- a/app/views/harvests/_card.html.haml +++ b/app/views/harvests/_card.html.haml @@ -21,8 +21,8 @@ %dd= display_quantity(harvest) %dt Harvest date : %dd= harvest.harvested_at - %dd Notes: - %dt=display_harvest_description(harvest) + %dt Notes: + %dd= display_harvest_description(harvest) - if harvest.planting.present? %dt Harvested from %dd= link_to(harvest.planting, planting_path(harvest.planting)) diff --git a/app/views/members/_gardens.html.haml b/app/views/members/_gardens.html.haml index 7e1bc61ca..5872f4926 100644 --- a/app/views/members/_gardens.html.haml +++ b/app/views/members/_gardens.html.haml @@ -1,4 +1,3 @@ -%h2 #{member.login_name}'s gardens .tabbable %ul.nav.nav-tabs - first_garden = true @@ -45,10 +44,10 @@ = link_to "Add photo", new_photo_path(type: "garden", id: g.id), class: 'btn btn-primary' %h3 What's planted here? - .row + .card-row - unless g.featured_plantings.empty? - g.featured_plantings.each.with_index do |planting| - .col-xs-12.col-lg-6 + .card = render partial: "plantings/card", locals: { planting: planting } %p diff --git a/app/views/members/_harvests.html.haml b/app/views/members/_harvests.html.haml new file mode 100644 index 000000000..248510c27 --- /dev/null +++ b/app/views/members/_harvests.html.haml @@ -0,0 +1,6 @@ +.card-row-short + - harvests.each do |harvest| + .card + = render partial: "harvests/card", locals: { harvest: harvest } +- if !harvests.any? + #{member.login_name} hasn't harvested anything yet. diff --git a/app/views/members/_map.html.haml b/app/views/members/_map.html.haml index 253f3b3ec..711e4d9e0 100644 --- a/app/views/members/_map.html.haml +++ b/app/views/members/_map.html.haml @@ -1,6 +1,6 @@ - if member.latitude && member.longitude #membermap - %p + %p.pull-right See other members, plantings, seeds and more near = link_to member.location, place_path(member.location, anchor: "members") - else diff --git a/app/views/members/_stats.html.haml b/app/views/members/_stats.html.haml index a2ebab47d..2db447d4c 100644 --- a/app/views/members/_stats.html.haml +++ b/app/views/members/_stats.html.haml @@ -7,7 +7,7 @@ %h3 Activity -%ul.list-inline +%ul.activity-list %li - if !member.plantings.empty? = link_to localize_plural(member.plantings, Planting), plantings_by_owner_path(owner: member) diff --git a/app/views/members/show.html.haml b/app/views/members/show.html.haml index 478574b3b..621f35c51 100644 --- a/app/views/members/show.html.haml +++ b/app/views/members/show.html.haml @@ -9,31 +9,40 @@ = tag("meta", property: "og:site_name", content: ENV['GROWSTUFF_SITE_NAME']) - content_for :buttonbar do - if can? :update, @member - = link_to 'Edit profile', edit_member_registration_path, class: 'btn btn-default' + = link_to 'Edit profile', edit_member_registration_path, class: 'btn btn-default pull-right' - if can?(:create, Notification) && current_member != @member - = link_to 'Send message', new_notification_path(recipient_id: @member.id), class: 'btn btn-default' + = link_to 'Send message', new_notification_path(recipient_id: @member.id), class: 'btn btn-default pull-right' - if current_member && current_member != @member # must be logged in, can't follow yourself - follow = current_member.get_follow(@member) - if !follow && can?(:create, Follow) # not already following - = link_to 'Follow', follows_path(followed_id: @member.id), method: :post, class: 'btn btn-default' + = link_to 'Follow', follows_path(followed_id: @member.id), method: :post, class: 'btn btn-default pull-right' - if follow && can?(:destroy, follow) # already following - = link_to 'Unfollow', follow_path(follow), method: :delete, class: 'btn btn-default' + = link_to 'Unfollow', follow_path(follow), method: :delete, class: 'btn btn-default pull-right' - content_for :member_rss_login_name, @member.login_name - content_for :member_rss_slug, @member.slug .row - - .col-md-9 - = render partial: "map", locals: { member: @member } - = render partial: "bio", locals: { member: @member } - = render partial: "gardens", locals: { member: @member, gardens: @gardens } - .col-md-3 + = render partial: "map", locals: { member: @member } + .col-md-2.profile-sidebar = render partial: "avatar", locals: { member: @member } + = render partial: "bio", locals: { member: @member } = render partial: "roles", locals: { member: @member } = render partial: "stats", locals: { member: @member } = render partial: "contact", locals: { member: @member, twitter_auth: @twitter_auth, flickr_auth: @flickr_auth, facebook_auth: @facebook_auth } + + .col-md-10 + %ul.nav.nav-pills.nav-justified + %li.active + %a{ "data-toggle" => "tab", href: "#gardens" } Gardens + %li + %a{ "data-toggle" => "tab", href: "#harvests" } Harvests + .tab-content.profile-activity + .tab-pane.active#gardens + = render partial: "gardens", locals: { member: @member, gardens: @gardens } + .tab-pane#harvests + = render partial: "harvests", locals: { member: @member, harvests: @harvests } diff --git a/app/views/plantings/_actions.html.haml b/app/views/plantings/_actions.html.haml index 87eddc8cf..6f5b1b93d 100644 --- a/app/views/plantings/_actions.html.haml +++ b/app/views/plantings/_actions.html.haml @@ -8,5 +8,5 @@ = render 'shared/buttons/harvest_planting', planting: planting = render 'shared/buttons/save_seeds', planting: planting - - if can? :destroy, planting - = render 'shared/buttons/delete', path: planting + - if can? :destroy, planting + = render 'shared/buttons/delete', path: planting diff --git a/app/views/plantings/_badges.html.haml b/app/views/plantings/_badges.html.haml new file mode 100644 index 000000000..2d690b914 --- /dev/null +++ b/app/views/plantings/_badges.html.haml @@ -0,0 +1,20 @@ +// Finish times +- if planting.finish_is_predicatable? + - if planting.super_late? + %span.badge.badge-super-late= t('.super_late') + = render 'shared/buttons/finish_planting', planting: planting + - elsif planting.late? + %span.badge.badge-late= t('.late_finishing') + - else + %span.badge + = days_from_now_to_finished(planting) + = t('.days_until_finished') + +// Harvest times +- unless planting.super_late? + - if planting.harvest_time? + %span.badge.badge-harvest= t('.harvesting_now') + - elsif planting.before_harvest_time? + %span.badge + = days_from_now_to_first_harvest(planting) + = t('.days_until_harvest') diff --git a/app/views/plantings/_progress.html.haml b/app/views/plantings/_progress.html.haml index a594de75d..5d94de1c3 100644 --- a/app/views/plantings/_progress.html.haml +++ b/app/views/plantings/_progress.html.haml @@ -1,8 +1,8 @@ - if planting.crop.perennial - %p Perennial + = render "plantings/progress_bar", status: "perennial", progress: nil - elsif !planting.planted? - if show_explanation - %p Progress: 0% - not planted yet + %p= t('.progress_0_not_planted_yet') = render "plantings/progress_bar", status: "not planted", progress: 0 - elsif planting.finished? = render "plantings/progress_bar", status: 'finished', progress: 100 diff --git a/app/views/plantings/_thumbnail.html.haml b/app/views/plantings/_thumbnail.html.haml index e34c653da..a6b8349b8 100644 --- a/app/views/plantings/_thumbnail.html.haml +++ b/app/views/plantings/_thumbnail.html.haml @@ -1,10 +1,13 @@ -.thumbnail - .planting-thumbnail - - if planting - = link_to image_tag(planting_image_path(planting), - alt: planting.crop.name, class: 'img'), - planting - .plantinginfo +.planting + .planting-badges + = render 'plantings/badges', planting: planting + .hover-wrapper + .thumbnail + .planting-thumbnail{ class: planting_classes(planting) } + = link_to image_tag(planting_image_path(planting), + alt: planting.crop.name, class: 'img'), planting_path(planting) + = render 'plantings/progress', planting: planting, show_explanation: false .planting-name - = render 'plantings/progress', planting: planting, show_explanation: false = link_to planting.crop.name, planting + .text + .planting-actions= render 'plantings/actions', planting: planting diff --git a/app/views/plantings/index.html.haml b/app/views/plantings/index.html.haml index 3302bdacb..2dc58ccde 100644 --- a/app/views/plantings/index.html.haml +++ b/app/views/plantings/index.html.haml @@ -3,7 +3,7 @@ = render 'nav', owner: @owner, show_all: @show_all - if @owner - = link_to "View #{@owner}'s profile >>", member_path(@owner) + = link_to t('.view_owners_profile', owner: @owner), member_path(@owner) .pagination = page_entries_info @plantings @@ -20,12 +20,7 @@ = will_paginate @plantings %ul.list-inline - %li The data on this page is available in the following formats: - - if @owner - %li= link_to "CSV", plantings_by_owner_path(@owner, format: 'csv') - %li= link_to "JSON", plantings_by_owner_path(@owner, format: 'json') - %li= link_to "RSS", plantings_by_owner_path(@owner, format: 'rss') - - else - %li= link_to "CSV", plantings_path(format: 'csv') - %li= link_to "JSON", plantings_path(format: 'json') - %li= link_to "RSS", plantings_path(format: 'rss') + %li= t('.the_data_on_this_page_is_available_in_the_following_formats') + - ['csv', 'json', 'rss'].each do |format| + %li= link_to format.upcase, + (@owner ? plantings_by_owner_path(@owner, format: format) : plantings_path(format: format)) diff --git a/app/views/shared/buttons/_finish_planting.html.haml b/app/views/shared/buttons/_finish_planting.html.haml index 9a3b6ce75..7b1ba8319 100644 --- a/app/views/shared/buttons/_finish_planting.html.haml +++ b/app/views/shared/buttons/_finish_planting.html.haml @@ -1,4 +1,4 @@ -- unless planting.finished +- if can?(:edit, planting) && !planting.finished = link_to planting_path(planting, planting: { finished: 1 }), method: :put, class: 'btn btn-default btn-xs append-date' do %span.glyphicon.glyphicon-ok{ title: "Finished" } diff --git a/config/locales/en.yml b/config/locales/en.yml index d5d3364a6..c494975e7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -52,6 +52,11 @@ en: location_helper: If you have a location set in your profile, it will be used when you create a new garden. location: "%{owner}'s %{garden}" updated: Garden was successfully updated. + overview: + gardensphoto: gardens/photo + plantingsthumbnail: plantings/thumbnail + no_plantings: no plantings + gardensactions: gardens/actions harvests: created: Harvest was successfully created. index: @@ -188,7 +193,18 @@ en: crop_plantings: Everyone's %{crop} plantings default: Everyone's plantings owner_plantings: "%{owner} plantings" + view_owners_profile: View %{owner}'s profile >> + the_data_on_this_page_is_available_in_the_following_formats: 'The data on this page is available in the following formats:' string: "%{crop} planting in %{garden} by %{owner}" + badges: + late_finishing: late finishing + super_late: super late + sharedbuttonsfinish_planting: shared/buttons/finish_planting + days_until_finished: days until finished + harvesting_now: harvesting now + days_until_harvest: days until harvest + progress: + progress_0_not_planted_yet: 'Progress: 0% - not planted yet' posts: index: title: diff --git a/db/migrate/20171105011017_set_prediction_data.rb b/db/migrate/20171105011017_set_prediction_data.rb index 149001927..5f5590d37 100644 --- a/db/migrate/20171105011017_set_prediction_data.rb +++ b/db/migrate/20171105011017_set_prediction_data.rb @@ -1,7 +1,7 @@ class SetPredictionData < ActiveRecord::Migration def up say "Updating all plantings time to first harvest" - Planting.all.each(&:update_harvest_days) + Planting.all.each(&:update_harvest_days!) say "Updating crop median time to first harvest, and lifespan" Crop.all.each do |crop| crop.update_lifespan_medians diff --git a/spec/factories/garden.rb b/spec/factories/garden.rb index 1f3d7461d..f93107361 100644 --- a/spec/factories/garden.rb +++ b/spec/factories/garden.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory :garden do - name 'Springfield Community Garden' + name { Faker::Vehicle.vin } description "This is a **totally** cool garden" owner active true diff --git a/spec/factories/planting.rb b/spec/factories/planting.rb index 5b7311025..27a2b179d 100644 --- a/spec/factories/planting.rb +++ b/spec/factories/planting.rb @@ -6,6 +6,8 @@ FactoryBot.define do planted_at { Time.zone.local(2014, 7, 30) } quantity 33 description "This is a *really* good plant." + finished false + finished_at nil factory :seed_planting do planted_from 'seed' diff --git a/spec/features/gardens/gardens_index_spec.rb b/spec/features/gardens/gardens_index_spec.rb index 39161d5c8..0a973e305 100644 --- a/spec/features/gardens/gardens_index_spec.rb +++ b/spec/features/gardens/gardens_index_spec.rb @@ -3,14 +3,13 @@ require 'custom_matchers' feature "Gardens#index", :js do context "Logged in as member" do - let(:member) { FactoryBot.create :member } - + let(:member) { FactoryBot.create :member, login_name: 'shadow' } background { login_as member } context "with 10 gardens" do before do FactoryBot.create_list :garden, 10, owner: member - visit gardens_path(member: member) + visit gardens_path(owner: member.login_name) end it "displays each of the gardens" do @@ -67,4 +66,70 @@ feature "Gardens#index", :js do end end end + + describe 'badges' do + let(:garden) { member.gardens.first } + let(:member) { FactoryBot.create :member, login_name: 'robbieburns' } + let(:crop) { FactoryBot.create :crop } + before(:each) do + # time to harvest = 50 day + # time to finished = 90 days + FactoryBot.create(:harvest, + harvested_at: 50.days.ago, + crop: crop, + planting: FactoryBot.create(:planting, + crop: crop, + planted_at: 100.days.ago, + finished_at: 10.days.ago)) + crop.plantings.each(&:update_harvest_days!) + crop.update_lifespan_medians + crop.update_harvest_medians + + garden.update! name: 'super awesome garden' + assert planting + visit gardens_path(owner: member.login_name) + end + + describe 'harvest still growing' do + let!(:planting) do + FactoryBot.create :planting, + crop: crop, + owner: member, + garden: garden, + planted_at: Time.zone.today + end + it { expect(page).to have_link href: planting_path(planting) } + it { expect(page).to have_link href: garden_path(planting.garden) } + it { expect(page).to have_text '50 days until harvest' } + it { expect(page).to have_text '90 days until finished' } + it { expect(page).not_to have_text 'harvesting now' } + end + + describe 'harvesting now' do + let!(:planting) do + FactoryBot.create :planting, + crop: crop, + owner: member, garden: garden, + planted_at: 51.days.ago + end + it { expect(crop.median_days_to_first_harvest).to eq 50 } + it { expect(crop.median_lifespan).to eq 90 } + + it { expect(page).to have_text 'harvesting now' } + it { expect(page).to have_text '39 days until finished' } + it { expect(page).not_to have_text 'days until harvest' } + end + + describe 'super late' do + let!(:planting) do + FactoryBot.create :planting, + crop: crop, owner: member, + garden: garden, planted_at: 260.days.ago + end + it { expect(page).to have_text 'super late' } + it { expect(page).not_to have_text 'harvesting now' } + it { expect(page).not_to have_text 'days until harvest' } + it { expect(page).not_to have_text 'days until finished' } + end + end end diff --git a/spec/features/harvests/harvesting_a_crop_spec.rb b/spec/features/harvests/harvesting_a_crop_spec.rb index e99c5e814..5f6d85fa2 100644 --- a/spec/features/harvests/harvesting_a_crop_spec.rb +++ b/spec/features/harvests/harvesting_a_crop_spec.rb @@ -130,7 +130,7 @@ feature "Harvesting a crop", :js, :elasticsearch do end scenario "linking to a planting" do - expect(page).to have_content planting.to_s + expect(page).to have_content existing_planting.to_s choose("harvest_planting_id_#{existing_planting.id}") click_button "save" expect(page).to have_link(href: planting_path(existing_planting)) diff --git a/spec/features/member_profile_spec.rb b/spec/features/member_profile_spec.rb index 9f1005894..db7d4f595 100644 --- a/spec/features/member_profile_spec.rb +++ b/spec/features/member_profile_spec.rb @@ -9,7 +9,6 @@ feature "member profile", js: true do expect(page).to have_css("h1", text: member.login_name) expect(page).to have_content member.bio expect(page).to have_content "Member since: #{member.created_at.to_s(:date)}" - expect(page).to have_content "#{member.login_name}'s gardens" expect(page).to have_link "More about this garden...", href: garden_path(member.gardens.first) end diff --git a/spec/models/garden_spec.rb b/spec/models/garden_spec.rb index f8e5a3311..839ef3445 100644 --- a/spec/models/garden_spec.rb +++ b/spec/models/garden_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' describe Garden do let(:owner) { FactoryBot.create(:member) } - let(:garden) { FactoryBot.create(:garden, owner: owner) } + let(:garden) { FactoryBot.create(:garden, owner: owner, name: 'Springfield Community Garden') } it "should have a slug" do garden.slug.should match(/member\d+-springfield-community-garden/) diff --git a/spec/models/planting_spec.rb b/spec/models/planting_spec.rb index 6948caa75..6b49f1c0a 100644 --- a/spec/models/planting_spec.rb +++ b/spec/models/planting_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' describe Planting do let(:crop) { FactoryBot.create(:tomato) } let(:garden_owner) { FactoryBot.create(:member) } - let(:garden) { FactoryBot.create(:garden, owner: garden_owner) } + let(:garden) { FactoryBot.create(:garden, owner: garden_owner, name: 'Springfield Community Garden') } let(:planting) { FactoryBot.create(:planting, crop: crop, garden: garden, owner: garden.owner) } let(:finished_planting) do FactoryBot.create :planting, planted_at: 4.days.ago, finished_at: 2.days.ago, finished: true @@ -60,9 +60,26 @@ describe Planting do describe 'planting 30 days ago, not finished' do let(:planting) { FactoryBot.create :planting, planted_at: 30.days.ago } - # 30 / 50 + # 30 / 50 = 60% it { expect(planting.percentage_grown).to eq 60.0 } + # planted 30 days ago it { expect(planting.days_since_planted).to eq 30 } + # means 20 days to go + it { expect(planting.finish_predicted_at).to eq Time.zone.today + 20.days } + end + + describe 'child crop uses parent data' do + let(:child_crop) { FactoryBot.create :crop, parent: crop, name: 'child' } + let(:child_planting) { FactoryBot.create :planting, crop: child_crop, planted_at: 30.days.ago } + + # not data for this crop + it { expect(child_crop.median_lifespan).to eq nil } + # 30 / 50 = 60% + it { expect(child_planting.percentage_grown).to eq 60.0 } + # planted 30 days ago + it { expect(child_planting.days_since_planted).to eq 30 } + # means 20 days to go + it { expect(child_planting.finish_predicted_at).to eq Time.zone.today + 20.days } end describe 'planting not planted yet' do @@ -73,8 +90,7 @@ describe Planting do describe 'planting finished 10 days, but was never planted' do let(:planting) { FactoryBot.create :planting, planted_at: nil, finished_at: 10.days.ago } - - it { expect(planting.percentage_grown).to eq nil } + it { expect(planting.percentage_grown).to eq 100 } end describe 'planted 30 days ago, finished 10 days ago' do @@ -97,8 +113,10 @@ describe Planting do it { expect(planting.expected_lifespan).to eq(nil) } end context 'lots of data' do + let(:crop) { FactoryBot.create :crop } + # this is a method so it creates a new one each time def one_hundred_day_old_planting - FactoryBot.create(:planting, crop: planting.crop, planted_at: 100.days.ago) + FactoryBot.create(:planting, crop: crop, planted_at: 100.days.ago) end before do # 50 days to harvest @@ -110,19 +128,35 @@ describe Planting do # 10 days to harvest FactoryBot.create(:harvest, harvested_at: 90.days.ago, crop: planting.crop, planting: one_hundred_day_old_planting) - planting.crop.plantings.each(&:update_harvest_days) + + planting.crop.plantings.each(&:update_harvest_days!) planting.crop.update_lifespan_medians planting.crop.update_harvest_medians end - it { expect(planting.crop.median_days_to_first_harvest).to eq(20) } + it { expect(crop.median_days_to_first_harvest).to eq(20) } + describe 'sets median time to harvest' do + let(:planting) { FactoryBot.create :planting, crop: crop, planted_at: Time.zone.today } + it { expect(planting.first_harvest_predicted_at).to eq(Time.zone.today + 20.days) } + end + + describe 'harvest still growing' do + let(:planting) { FactoryBot.create :planting, crop: crop, planted_at: Time.zone.today } + it { expect(planting.before_harvest_time?).to eq true } + it { expect(planting.harvest_time?).to eq false } + end + describe 'harvesting ready now' do + let(:planting) { FactoryBot.create :planting, crop: crop, planted_at: 21.days.ago } + it { expect(planting.first_harvest_predicted_at).to eq(1.day.ago.to_date) } + it { expect(planting.before_harvest_time?).to eq false } + it { expect(planting.harvest_time?).to eq true } + end end describe 'planting has no harvests' do + let(:planting) { FactoryBot.create :planting } before do - planting.update_harvest_days + planting.update_harvest_days! planting.crop.update_harvest_medians end - let(:planting) { FactoryBot.create :planting } - it { expect(planting.days_to_first_harvest).to eq(nil) } it { expect(planting.days_to_last_harvest).to eq(nil) } end @@ -134,7 +168,7 @@ describe Planting do planting: planting, crop: planting.crop, harvested_at: 10.days.ago) - planting.update_harvest_days + planting.update_harvest_days! planting.crop.update_harvest_medians end it { expect(planting.days_to_first_harvest).to eq(90) } @@ -148,7 +182,7 @@ describe Planting do before do FactoryBot.create :harvest, planting: planting, crop: planting.crop, harvested_at: 90.days.ago FactoryBot.create :harvest, planting: planting, crop: planting.crop, harvested_at: 10.days.ago - planting.update_harvest_days + planting.update_harvest_days! planting.crop.update_harvest_medians end it { expect(planting.days_to_first_harvest).to eq(10) }