diff --git a/.travis.yml b/.travis.yml index 65db1b3f9..323d3a097 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,46 +7,37 @@ cache: - tmp/cache/assets/test/sprockets env: matrix: - - GROWSTUFF_ELASTICSEARCH='true' RSPEC_TAG=elasticsearch STATIC_CHECKS=false - - GROWSTUFF_ELASTICSEARCH='false' RSPEC_TAG=~elasticsearch STATIC_CHECKS=false + - GROWSTUFF_ELASTICSEARCH=true RSPEC_TAG=elasticsearch COVERAGE=true + - GROWSTUFF_ELASTICSEARCH=false RSPEC_TAG=~elasticsearch COVERAGE=false - STATIC_CHECKS=true global: - GROWSTUFF_SITE_NAME="Growstuff (travis)" - RAILS_SECRET_TOKEN='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' - secure: "Z5TpM2jEX4UCvNePnk/LwltQX48U2u9BRc+Iypr1x9QW2o228QJhPIOH39a8RMUrepGnkQIq9q3ZRUn98RfrJz1yThtlNFL3NmzdQ57gKgjGwfpa0e4Dwj/ZJqV2D84tDGjvdVYLP7zzaYZxQcwk/cgNpzKf/jq97HLNP7CYuf4=" before_install: - - ./script/install_phantomjs; + - ./script/install_phantomjs.sh - export PATH=$PWD/travis_phantomjs/phantomjs-2.1.1-linux-x86_64/bin:$PATH + - ./script/install_codeclimate.sh + - ./script/install_linters.sh # Force Travis to use Elastic Search 2.4.0 - - > - if [ "${GROWSTUFF_ELASTICSEARCH}" = "true" ]; then - sudo dpkg -r elasticsearch; - curl -O https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/deb/elasticsearch/2.4.0/elasticsearch-2.4.0.deb; - sudo dpkg -i --force-confnew elasticsearch-2.4.0.deb; - sudo service elasticsearch start; - sleep 10; - curl localhost:9200; - fi -before_script: - - set -e - - > - if [ "${STATIC_CHECKS}" = "true" ]; then - ./script/install_linters; - else - RAILS_ENV=test bundle exec rake db:create db:migrate; - bundle exec rake assets:precompile; - fi - - set +e + - ./script/install_elasticsearch.sh script: - set -e - > if [ "${STATIC_CHECKS}" = "true" ]; then ./script/check_static.rb else - bundle exec rake db:migrate --trace; + set +e; + RAILS_ENV=test bundle exec rake db:create db:migrate; + bundle exec rake assets:precompile; bundle exec rspec --tag $RSPEC_TAG spec/; fi; - set +e +after_script: + - > + if [ "${COVERAGE}" = "true" ]; then + ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; + fi before_deploy: - bundle exec script/heroku_maintenance.rb on deploy: @@ -69,4 +60,5 @@ after_deploy: - bundle exec script/heroku_maintenance.rb off addons: code_climate: - repo_token: 462e015bbdaabfb20910fc07f2fea253410ecb131444e00f97dbf32dc6789ca6 + repo_token: + secure: "PfhLGBKRgNqhKuYCJsK+VPhdAzcgWFGeeOyxC/eS8gtlvIISVdgyZE+r30uIei0DFI6zEiN62eW4d+xtT4j7/e2ZcAcx7U52mza/SnQNuu3nCGQDJB8VOvV5NbnwXfi8vfr4e889Mt7k3ocd2c4gqB4UtRqrzhygj7HN+B/GfEk=" diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 568137391..87b153b17 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -83,6 +83,7 @@ submit the change with your pull request. - Jeff Kingswood / [ancyentmariner](https://github.com/ancyentmariner) - Logan Gingerich / [logangingerich](https://github.com/logangingerich) - Mark Taffman / [mftaff](https://github.com/mftaff) +- Jennifer Kruse / [jenkr55](https://github.com/jenkr55) ## Bots diff --git a/Gemfile b/Gemfile index 42b698280..cecb3d65e 100644 --- a/Gemfile +++ b/Gemfile @@ -86,9 +86,9 @@ gem "chartkick" # Project does not use semver, so we want to be in sync with the version of # elasticsearch we use # See https://github.com/elastic/elasticsearch-ruby#compatibility -gem "elasticsearch-api" -gem "elasticsearch-model" -gem "elasticsearch-rails" +gem "elasticsearch-api", "~> 2.0.0" +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 be1b6485e..e153efdb1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -48,7 +48,7 @@ GEM public_suffix (>= 2.0.2, < 4.0) arel (8.0.0) ast (2.4.0) - autoprefixer-rails (8.2.0) + autoprefixer-rails (8.4.1) execjs bcrypt (3.1.11) better_errors (2.4.0) @@ -70,7 +70,7 @@ GEM activesupport (>= 3.0.0) uniform_notifier (~> 1.11.0) byebug (10.0.2) - cancancan (2.1.3) + cancancan (2.2.0) capybara (2.18.0) addressable mini_mime (>= 0.1.3) @@ -78,13 +78,13 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (>= 2.0, < 4.0) - capybara-email (2.5.0) - capybara (~> 2.4) + capybara-email (3.0.1) + capybara (>= 2.4, < 4.0) mail - capybara-screenshot (1.0.18) - capybara (>= 1.0, < 3) + capybara-screenshot (1.0.19) + capybara (>= 1.0, < 4) launchy - chartkick (2.3.3) + chartkick (2.3.4) childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) cliver (0.3.2) @@ -106,11 +106,11 @@ GEM term-ansicolor (~> 1.3) thor (~> 0.19.1) tins (~> 1.6) - crass (1.0.3) + crass (1.0.4) csv_shaper (1.3.0) activesupport (>= 3.0.0) - dalli (2.7.7) - database_cleaner (1.6.2) + dalli (2.7.8) + database_cleaner (1.7.0) devise (4.4.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -122,17 +122,17 @@ GEM easy_translate (0.5.1) thread thread_safe - elasticsearch (5.0.4) - elasticsearch-api (= 5.0.4) - elasticsearch-transport (= 5.0.4) - elasticsearch-api (5.0.4) + elasticsearch (2.0.2) + elasticsearch-api (= 2.0.2) + elasticsearch-transport (= 2.0.2) + elasticsearch-api (2.0.2) multi_json - elasticsearch-model (5.0.2) + elasticsearch-model (5.0.0) activesupport (> 3) - elasticsearch (~> 5) + elasticsearch (> 1) hashie elasticsearch-rails (5.0.2) - elasticsearch-transport (5.0.4) + elasticsearch-transport (2.0.2) faraday multi_json erubi (1.7.1) @@ -154,7 +154,7 @@ GEM flickraw (0.9.9) font-awesome-sass (5.0.9) sass (>= 3.2) - friendly_id (5.2.3) + friendly_id (5.2.4) activerecord (>= 4.0.0) geocoder (1.4.7) gibbon (1.2.1) @@ -202,17 +202,17 @@ GEM multi_xml (>= 0.5.2) i18n (0.9.5) concurrent-ruby (~> 1.0) - i18n-tasks (0.9.12) + i18n-tasks (0.9.21) activesupport (>= 4.0.2) ast (>= 2.1.0) - easy_translate (>= 0.5.0) - erubis + easy_translate (>= 0.5.1) + erubi highline (>= 1.7.3) i18n parser (>= 2.2.3.0) - term-ansicolor (>= 1.3.2) + rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) - jquery-rails (4.3.1) + jquery-rails (4.3.3) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) @@ -278,7 +278,7 @@ GEM omniauth (1.8.1) hashie (>= 3.4.6, < 3.6.0) rack (>= 1.6.2, < 3) - omniauth-facebook (4.0.0) + omniauth-facebook (5.0.0) omniauth-oauth2 (~> 1.2) omniauth-flickr (0.0.19) multi_json (~> 1.11.0) @@ -294,9 +294,9 @@ GEM rack orm_adapter (0.5.0) parallel (1.12.1) - paranoia (2.4.0) - activerecord (>= 4.0, < 5.2) - parser (2.5.0.5) + paranoia (2.4.1) + activerecord (>= 4.0, < 5.3) + parser (2.5.1.0) ast (~> 2.4.0) pg (0.21.0) platform-api (2.1.0) @@ -308,7 +308,7 @@ GEM websocket-driver (>= 0.2.0) powerpack (0.1.1) public_suffix (3.0.2) - rack (2.0.4) + rack (2.0.5) rack-protection (2.0.1) rack rack-test (1.0.0) @@ -379,7 +379,7 @@ GEM rspec-mocks (~> 3.7.0) rspec-support (~> 3.7.0) rspec-support (3.7.1) - rubocop (0.54.0) + rubocop (0.55.0) parallel (~> 1.10) parser (>= 2.5) powerpack (~> 0.1) @@ -406,8 +406,8 @@ GEM selenium-webdriver (3.11.0) childprocess (~> 0.5) rubyzip (~> 1.2) - sexp_processor (4.10.1) - sidekiq (5.1.2) + sexp_processor (4.11.0) + sidekiq (5.1.3) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) @@ -417,8 +417,8 @@ GEM json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - sparkpost_rails (1.5.0) - rails (>= 4.0, < 5.2) + sparkpost_rails (1.5.1) + rails (>= 4.0, < 5.3) sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -441,9 +441,9 @@ GEM trollop (1.16.2) tzinfo (1.2.5) thread_safe (~> 0.1) - uglifier (4.1.8) + uglifier (4.1.10) execjs (>= 0.3.0, < 3) - unicode-display_width (1.3.0) + unicode-display_width (1.3.2) unicorn (5.4.0) kgio (~> 2.6) raindrops (~> 0.7) @@ -489,9 +489,9 @@ DEPENDENCIES dalli database_cleaner devise - elasticsearch-api - elasticsearch-model - elasticsearch-rails + elasticsearch-api (~> 2.0.0) + elasticsearch-model (>= 5.0.0) + elasticsearch-rails (>= 5.0.0) factory_bot_rails faker figaro 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 338a2dbc5..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 @@ -73,10 +92,16 @@ p.stats display: flex flex: none flex-wrap: wrap - justify-content: space-between + +.card-row + display: grid + grid-template-columns: 50% 50% + grid-gap: 25px + grid-row-gap: 5px .member-thumbnail padding: .25em + margin: 1em div width: 5em @@ -87,19 +112,47 @@ 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 dd margin-left: auto +.layout-actions + width: 100% #placesmap, #cropmap height: 500px #membermap height: 250px + z-index: 0 .location-not-set height: 250px @@ -146,6 +199,8 @@ p.stats border: none text-align: center margin-bottom: 1.5em + max-width: 160px + max-height: 200px .member-thumbnail text-align: left @@ -192,6 +247,9 @@ li.crop-hierarchy .navbar-bottom margin: 40px 0px 0px 0px !important +.post-actions + margin-bottom: 1rem + // footer footer #footer1, #footer2, #footer3 @@ -221,6 +279,7 @@ footer #maincontainer min-height: 80% + padding: 50px html, body height: 100% @@ -334,3 +393,6 @@ ul.thumbnail-buttons height: 180px .seed-thumbnail height: 220px + + #maincontainer + padding: 10px diff --git a/app/controllers/harvests_controller.rb b/app/controllers/harvests_controller.rb index 55254516a..7c64d42d3 100644 --- a/app/controllers/harvests_controller.rb +++ b/app/controllers/harvests_controller.rb @@ -93,7 +93,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 0fa013649..192663c2e 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/application_helper.rb b/app/helpers/application_helper.rb index ad63f37e2..dad3eab2b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -84,11 +84,15 @@ module ApplicationHelper def show_inactive_tickbox_path(type, owner, show_all) all = show_all ? '' : 1 if owner - plantings_by_owner_path(owner: owner.slug, all: all) if type == 'plantings' - gardens_by_owner_path(owner: owner.slug, all: all) if type == 'gardens' - else - plantings_path(all: all) if type == 'plantings' - gardens_path(all: all) if type == 'gardens' + if type == 'plantings' + plantings_by_owner_path(owner: owner.slug, all: all) + elsif type == 'gardens' + gardens_by_owner_path(owner: owner.slug, all: all) + end + elsif type == 'plantings' + plantings_path(all: all) + elsif type == 'gardens' + gardens_path(all: all) end end diff --git a/app/helpers/photos_helper.rb b/app/helpers/photos_helper.rb index b77f35bd9..6087a9c74 100644 --- a/app/helpers/photos_helper.rb +++ b/app/helpers/photos_helper.rb @@ -17,7 +17,7 @@ module PhotosHelper def planting_image_path(planting) if planting.photos.present? - planting.photos.first.thumbnail_url + planting.photos.order(date_taken: :desc).first.thumbnail_url else placeholder_image end @@ -25,9 +25,9 @@ module PhotosHelper def harvest_image_path(harvest) if harvest.photos.present? - harvest.photos.first.thumbnail_url - elsif harvest.planting.present? && harvest.planting.photos.present? - harvest.planting.photos.first.thumbnail_url + harvest.photos.order(date_taken: :desc).first.thumbnail_url + elsif harvest.planting.present? + planting_image_path(harvest.planting) else placeholder_image end diff --git a/app/helpers/plantings_helper.rb b/app/helpers/plantings_helper.rb index 26d0ec6c4..5f448975e 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/ownable.rb b/app/models/concerns/ownable.rb new file mode 100644 index 000000000..b9f229973 --- /dev/null +++ b/app/models/concerns/ownable.rb @@ -0,0 +1,7 @@ +module Ownable + extend ActiveSupport::Concern + + included do + belongs_to :owner, class_name: 'Member', foreign_key: 'owner_id', counter_cache: true + 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 e41343253..a099e8c64 100644 --- a/app/models/crop.rb +++ b/app/models/crop.rb @@ -15,7 +15,7 @@ class Crop < ApplicationRecord has_many :seeds, dependent: :destroy has_many :harvests, dependent: :destroy has_many :photos, through: :plantings - has_many :plant_parts, -> { distinct.reorder("plant_parts.name") }, through: :harvests + has_many :plant_parts, -> { uniq.reorder("plant_parts.name") }, through: :harvests belongs_to :creator, class_name: 'Member', optional: true belongs_to :requester, class_name: 'Member', optional: true belongs_to :parent, class_name: 'Crop', optional: true @@ -26,13 +26,11 @@ class Crop < ApplicationRecord ## Scopes scope :recent, -> { approved.order(created_at: :desc) } scope :toplevel, -> { approved.where(parent_id: nil) } - scope :popular, -> { approved.reorder("plantings_count desc, lower(name) asc") } - # ok on sqlite and psql, but not on mysql - scope :randomized, -> { approved.reorder('random()') } + scope :popular, -> { approved.order("plantings_count desc, lower(name) asc") } scope :pending_approval, -> { where(approval_status: "pending") } scope :approved, -> { where(approval_status: "approved") } scope :rejected, -> { where(approval_status: "rejected") } - scope :interesting, -> { approved.has_photos.randomized } + scope :interesting, -> { approved.has_photos } scope :has_photos, -> { includes(:photos).where.not(photos: { id: nil }) } ## @@ -187,7 +185,7 @@ class Crop < ApplicationRecord 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/forum.rb b/app/models/forum.rb index 7dad73bc2..664af4f38 100644 --- a/app/models/forum.rb +++ b/app/models/forum.rb @@ -1,10 +1,10 @@ class Forum < ApplicationRecord extend FriendlyId + include Ownable validates :name, presence: true friendly_id :name, use: %i(slugged finders) has_many :posts, dependent: :destroy - belongs_to :owner, class_name: "Member" def to_s name diff --git a/app/models/garden.rb b/app/models/garden.rb index 2946af1cf..9687982cb 100644 --- a/app/models/garden.rb +++ b/app/models/garden.rb @@ -2,9 +2,9 @@ class Garden < ApplicationRecord extend FriendlyId include Geocodable include PhotoCapable + include Ownable friendly_id :garden_slug, use: %i(slugged finders) - belongs_to :owner, class_name: 'Member', foreign_key: 'owner_id', counter_cache: true has_many :plantings, dependent: :destroy has_many :crops, through: :plantings diff --git a/app/models/harvest.rb b/app/models/harvest.rb index 3a96afdbb..c9829fed7 100644 --- a/app/models/harvest.rb +++ b/app/models/harvest.rb @@ -2,6 +2,7 @@ class Harvest < ApplicationRecord include ActionView::Helpers::NumberHelper extend FriendlyId include PhotoCapable + include Ownable friendly_id :harvest_slug, use: %i(slugged finders) @@ -33,7 +34,6 @@ class Harvest < ApplicationRecord ## ## Relationships belongs_to :crop - belongs_to :owner, class_name: 'Member', counter_cache: true belongs_to :plant_part belongs_to :planting, optional: true diff --git a/app/models/photo.rb b/app/models/photo.rb index 67aac82f3..69a30d245 100644 --- a/app/models/photo.rb +++ b/app/models/photo.rb @@ -1,5 +1,5 @@ class Photo < ApplicationRecord - belongs_to :owner, class_name: 'Member' + include Ownable PHOTO_CAPABLE = %w(Garden Planting Harvest Seed).freeze diff --git a/app/models/planting.rb b/app/models/planting.rb index a4521f6ca..144d3ee42 100644 --- a/app/models/planting.rb +++ b/app/models/planting.rb @@ -2,6 +2,9 @@ class Planting < ApplicationRecord extend FriendlyId include PhotoCapable include Finishable + include Ownable + include PredictPlanting + include PredictHarvest friendly_id :planting_slug, use: %i(slugged finders) # Constants @@ -12,12 +15,7 @@ class Planting < ApplicationRecord 'graft', 'layering' ].freeze - ## - ## Triggers - before_save :calculate_lifespan - belongs_to :garden - belongs_to :owner, class_name: 'Member', counter_cache: true belongs_to :crop, counter_cache: true has_many :harvests, dependent: :destroy @@ -60,6 +58,10 @@ class Planting < ApplicationRecord 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, @@ -82,59 +84,20 @@ class Planting < ApplicationRecord 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 - return (finished_at - planted_at).to_i if planted_at.present? && finished_at.present? - 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/models/seed.rb b/app/models/seed.rb index ff687a4eb..8d33e7ff8 100644 --- a/app/models/seed.rb +++ b/app/models/seed.rb @@ -2,6 +2,7 @@ class Seed < ApplicationRecord extend FriendlyId include PhotoCapable include Finishable + include Ownable friendly_id :seed_slug, use: %i(slugged finders) TRADABLE_TO_VALUES = %w(nowhere locally nationally internationally).freeze @@ -12,8 +13,6 @@ class Seed < ApplicationRecord # # Relationships belongs_to :crop - belongs_to :owner, class_name: 'Member', foreign_key: 'owner_id', counter_cache: true - belongs_to :parent_planting, class_name: 'Planting', foreign_key: 'parent_planting_id', required: false # parent has_many :child_plantings, class_name: 'Planting', diff --git a/app/views/crops/_plantings.html.haml b/app/views/crops/_plantings.html.haml index 351c6ba4c..ca05632a3 100644 --- a/app/views/crops/_plantings.html.haml +++ b/app/views/crops/_plantings.html.haml @@ -4,9 +4,9 @@ Nobody has planted this crop yet. - else %ul - - crop.plantings.take(3).each do |planting| + - crop.plantings.order(planted_at: :desc).limit(3).each do |planting| %li - = link_to display_planting(planting), planting_path(planting) + = link_to planting, planting_path(planting) = render partial: 'members/location', locals: { member: planting.owner } %small = distance_of_time_in_words(planting.created_at, Time.zone.now) @@ -18,4 +18,3 @@ %p= link_to "Plant #{crop.name}", new_planting_path(crop_id: crop.id) - else = render partial: 'shared/signin_signup', locals: { to: "track your #{crop.name} plantings" } - 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/harvests/_list.html.haml b/app/views/harvests/_list.html.haml index bb0178238..8e4e1a7a1 100644 --- a/app/views/harvests/_list.html.haml +++ b/app/views/harvests/_list.html.haml @@ -1,11 +1,10 @@ - harvests.each do |h| - cache h do .row - .col-md-3.col-xs-4{ style: 'padding-bottom: 6px' } + .col-lg-6.col-md-3.col-xs-4.homepage-listing = render 'harvests/image_with_popover', harvest: h - .col-md-9.col-xs-4 + .col-lg-3.col-md-9.col-xs-4 = link_to h.crop, crop_path(h.crop) %br/ %small - %i - = h.owner.location + %i= h.owner.location diff --git a/app/views/harvests/_planting.haml b/app/views/harvests/_planting.haml index 085f24c7c..6e6ba03c2 100644 --- a/app/views/harvests/_planting.haml +++ b/app/views/harvests/_planting.haml @@ -3,7 +3,7 @@ planting_path(@harvest.planting) in = link_to @harvest.planting.garden, garden_path(@harvest.planting.garden) -- elsif @matching_plantings && @harvest.owner == current_member +- elsif @matching_plantings && @matching_plantings.any? && @harvest.owner == current_member Is this from one of these plantings? = form_for(@harvest) do |f| - @matching_plantings.each do |planting| diff --git a/app/views/home/_blurb.html.haml b/app/views/home/_blurb.html.haml index 0459d1095..81e65510f 100644 --- a/app/views/home/_blurb.html.haml +++ b/app/views/home/_blurb.html.haml @@ -1,16 +1,13 @@ -.col-md-12 - -%h1= ENV['GROWSTUFF_SITE_NAME'] - -.row - .col-md-8.info - %p= t('.intro', site_name: ENV['GROWSTUFF_SITE_NAME']) - - = render partial: 'stats' - .col-md-4.signup - %p= t('.perks') - %p= link_to(t('.sign_up'), new_member_registration_path, class: 'btn btn-primary btn-lg') - %p - %small - = t('.already_html', sign_in: link_to(t('.sign_in_linktext'), new_member_session_path)) - +.container + .row + .col-md-12 + %h1= ENV['GROWSTUFF_SITE_NAME'] + .col-md-8.info + %p= t('.intro', site_name: ENV['GROWSTUFF_SITE_NAME']) + = render partial: 'stats' + .col-md-4.signup + %p= t('.perks') + %p= link_to(t('.sign_up'), new_member_registration_path, class: 'btn btn-primary btn-lg') + %p + %small + = t('.already_html', sign_in: link_to(t('.sign_in_linktext'), new_member_session_path)) diff --git a/app/views/home/_crops.html.haml b/app/views/home/_crops.html.haml index bfcbc9a71..8cffe4b78 100644 --- a/app/views/home/_crops.html.haml +++ b/app/views/home/_crops.html.haml @@ -1,5 +1,6 @@ - cache cache_key_for(Crop, 'interesting'), expires_in: 1.day do .row %h2= t('.our_crops') - - Crop.interesting.includes(:scientific_names, :photos).limit(8).each do |c| - .col-md-4.col-sm-3.col-xs-6= render 'crops/thumbnail', crop: c + - Crop.interesting.includes(:scientific_names, :photos).shuffle.first(12).each do |c| + .col-lg-2.col-md-4.col-sm-3.col-xs-6 + = render 'crops/thumbnail', crop: c diff --git a/app/views/home/_harvests.html.haml b/app/views/home/_harvests.html.haml index d4c2a1117..2229eae63 100644 --- a/app/views/home/_harvests.html.haml +++ b/app/views/home/_harvests.html.haml @@ -1,3 +1,3 @@ - cache cache_key_for(Harvest) do %h2 Recently Harvested - = render 'harvests/list', harvests: Harvest.includes(:crop, :owner, :photos).has_photos.recent.first(5) + = render 'harvests/list', harvests: Harvest.includes(:crop, :owner, :photos).has_photos.recent.first(6) diff --git a/app/views/home/_plantings.html.haml b/app/views/home/_plantings.html.haml index 595ad4586..dd631d2ff 100644 --- a/app/views/home/_plantings.html.haml +++ b/app/views/home/_plantings.html.haml @@ -1,3 +1,3 @@ - cache cache_key_for(Planting, 'home'), expires_in: 1.day do %h2= t('.recently_planted') - = render 'plantings/list', plantings: Planting.includes(:crop, garden: :owner).has_photos.recent.limit(5) + = render 'plantings/list', plantings: Planting.includes(:crop, garden: :owner).has_photos.recent.limit(6) diff --git a/app/views/home/_seeds.html.haml b/app/views/home/_seeds.html.haml index d53759738..4a28f9590 100644 --- a/app/views/home/_seeds.html.haml +++ b/app/views/home/_seeds.html.haml @@ -1,7 +1,7 @@ - cache cache_key_for(Seed, 'interesting'), expires_in: 1.day do %h2= t('.title') .row - - Seed.current.tradable.order(created_at: :desc).limit(6).each do |seed| + - Seed.current.tradable.includes(:owner, :crop).order(created_at: :desc).limit(6).each do |seed| .col-md-2.col-sm-2.col-xs-6 .thumbnail.seed-thumbnail - cache cache_key_for(Crop, seed.id) do diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index c28a8985b..d05d6a37a 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -19,11 +19,11 @@ = render 'blurb' .row - .col-md-6.col-sm-12 + .col-lg-8.col-md-6.col-sm-12 = render 'crops' - .col-md-3.col-sm-6 + .col-lg-2.col-md-3.col-sm-6 = render 'plantings' - .col-md-3.col-sm-6 + .col-lg-2.col-md-3.col-sm-6 = render 'harvests' .col-md-12 - cache cache_key_for(Crop, 'recent') do diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index f45392712..070ed73db 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -5,7 +5,7 @@ %body = render partial: "layouts/header" - .container#maincontainer + #maincontainer .row .col-md-12 - if content_for?(:title) 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/_image_with_popover.html.haml b/app/views/plantings/_image_with_popover.html.haml index 70bfc1836..740562fda 100644 --- a/app/views/plantings/_image_with_popover.html.haml +++ b/app/views/plantings/_image_with_popover.html.haml @@ -5,6 +5,6 @@ planting, rel: "popover", 'data-trigger': 'hover', - 'data-title': planting.to_s, + 'data-title': planting.crop.name, 'data-content': render('plantings/popover', planting: planting), 'data-html': true diff --git a/app/views/plantings/_list.html.haml b/app/views/plantings/_list.html.haml index aa7dcf9fa..21be75416 100644 --- a/app/views/plantings/_list.html.haml +++ b/app/views/plantings/_list.html.haml @@ -1,15 +1,10 @@ - plantings.each do |p| - cache p do .row - .col-md-3.col-xs-4.homepage-listing + .col-lg-6.col-md-3.col-xs-4.homepage-listing = render 'plantings/image_with_popover', planting: p - .col-md-9.col-xs-4 + .col-lg-3.col-md-9.col-xs-4 = link_to p.crop, p.crop - in - = succeed "'s" do - = link_to p.garden.owner, p.garden.owner - = link_to display_garden_name(p.garden), p.garden %br/ %small - %i - = p.location + %i= p.location diff --git a/app/views/plantings/_nav.haml b/app/views/plantings/_nav.haml index bb81da701..396d6e021 100644 --- a/app/views/plantings/_nav.haml +++ b/app/views/plantings/_nav.haml @@ -15,4 +15,3 @@ = link_to 'Plant something', new_planting_path, class: 'btn btn-primary' - else = render partial: 'shared/signin_signup', locals: { to: "track what you've planted" } - diff --git a/app/views/plantings/_popover.html.haml b/app/views/plantings/_popover.html.haml index d7a83f616..44fd1744f 100644 --- a/app/views/plantings/_popover.html.haml +++ b/app/views/plantings/_popover.html.haml @@ -1,7 +1,11 @@ -%p - %small - Quantity: - = planting.quantity ? planting.quantity : 'unknown' - %br/ - Planted on: - = planting.planted_at.to_s +- if planting.quantity.present? + %p + %small + Quantity: + = planting.quantity + +- if planting.planted_at.present? + %p + %small + Planted: + = planting.planted_at.to_s 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 8ad0277d2..2dc58ccde 100644 --- a/app/views/plantings/index.html.haml +++ b/app/views/plantings/index.html.haml @@ -3,16 +3,16 @@ = 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 = will_paginate @plantings -.row +.card-row - unless @plantings.empty? - @plantings.each.with_index do |planting| - .col-xs-12.col-lg-6 + .card = render partial: "plantings/card", locals: { planting: planting } .pagination @@ -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/posts/show.html.haml b/app/views/posts/show.html.haml index 3a50106a5..cca0f813d 100644 --- a/app/views/posts/show.html.haml +++ b/app/views/posts/show.html.haml @@ -36,8 +36,12 @@ = link_to 'Edit Post', edit_post_path(@post), class: 'btn btn-default btn-xs' - if can? :destroy, @post = link_to 'Delete Post', @post, method: :delete, - data: { confirm: 'Are you sure?' }, - class: 'btn btn-default btn-xs' + data: { confirm: 'Are you sure?' }, + class: 'btn btn-default btn-xs' + + - if @post.comments.count > 10 && can?(:create, Comment) + .post-actions + = link_to 'Comment', new_comment_path(post_id: @post.id), class: 'btn btn-primary' = render partial: "comments", locals: { post: @post } diff --git a/app/views/seeds/index.html.haml b/app/views/seeds/index.html.haml index e7e1ec3e3..5e2eabece 100644 --- a/app/views/seeds/index.html.haml +++ b/app/views/seeds/index.html.haml @@ -24,10 +24,10 @@ = page_entries_info @seeds = will_paginate @seeds -.row +.card-row - unless @seeds.empty? - @seeds.each do |seed| - .col-md-6 + .seedcard = render 'seeds/card', seed: seed .pagination 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 d964c8848..c1931b2b6 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[4.2] 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/db/migrate/20180401220637_add_member_count_caches.rb b/db/migrate/20180401220637_add_member_count_caches.rb new file mode 100644 index 000000000..912db008f --- /dev/null +++ b/db/migrate/20180401220637_add_member_count_caches.rb @@ -0,0 +1,6 @@ +class AddMemberCountCaches < ActiveRecord::Migration[4.2] + def change + add_column :members, :photos_count, :integer + add_column :members, :forums_count, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 0651dbc56..6640ce490 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180213005731) do +ActiveRecord::Schema.define(version: 20180401220637) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -299,6 +299,8 @@ ActiveRecord::Schema.define(version: 20180213005731) do t.integer "harvests_count" t.integer "seeds_count" t.datetime "deleted_at" + t.integer "photos_count" + t.integer "forums_count" t.index ["confirmation_token"], name: "index_members_on_confirmation_token", unique: true t.index ["deleted_at"], name: "index_members_on_deleted_at" t.index ["email"], name: "index_members_on_email", unique: true diff --git a/script/install_codeclimate.sh b/script/install_codeclimate.sh new file mode 100755 index 000000000..f48091a9a --- /dev/null +++ b/script/install_codeclimate.sh @@ -0,0 +1,7 @@ +#!/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/script/install_elasticsearch.sh b/script/install_elasticsearch.sh new file mode 100755 index 000000000..1543a71bc --- /dev/null +++ b/script/install_elasticsearch.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +if [ "${GROWSTUFF_ELASTICSEARCH}" = "true" ]; then + set -euv + sudo dpkg -r elasticsearch + curl -O https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/deb/elasticsearch/2.4.0/elasticsearch-2.4.0.deb + sudo dpkg -i --force-confnew elasticsearch-2.4.0.deb + sudo service elasticsearch start + sleep 10 + curl -v localhost:9200 +fi \ No newline at end of file diff --git a/script/install_linters b/script/install_linters deleted file mode 100755 index b1d7534b1..000000000 --- a/script/install_linters +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -euv - -gem install --update overcommit rubocop haml-lint bundler-audit -npm install -pip install yamllint --user - -overcommit --install -overcommit --sign -overcommit --sign pre-commit - -bundle-audit update diff --git a/script/install_linters.sh b/script/install_linters.sh new file mode 100755 index 000000000..679a6b64a --- /dev/null +++ b/script/install_linters.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +if [ "${STATIC_CHECKS}" = "true" ]; then + set -euv + gem install --update overcommit rubocop haml-lint bundler-audit; + npm install; + pip install yamllint --user; + + overcommit --install; + overcommit --sign; + overcommit --sign pre-commit; + + bundle-audit update; +fi diff --git a/script/install_phantomjs b/script/install_phantomjs.sh similarity index 100% rename from script/install_phantomjs rename to script/install_phantomjs.sh 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 3ee483c04..baa8c001a 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, login_name: 'hatupatu') } - 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(/hatupatu-springfield-community-garden/) diff --git a/spec/models/planting_spec.rb b/spec/models/planting_spec.rb index 42bf04a4b..3c76d6ed6 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, login_name: 'hatupatu') } - 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) }