Compare commits

..

1 Commits

Author SHA1 Message Date
Daniel O'Connor
fea2f6ff61 Fix deprecations 2025-08-27 14:28:14 +00:00
40 changed files with 117 additions and 170 deletions

View File

@@ -183,7 +183,7 @@ GEM
image_processing (~> 1.1)
marcel (~> 1.0.0)
ssrf_filter (~> 1.0)
chartkick (5.2.0)
chartkick (5.1.5)
childprocess (5.0.0)
coderay (1.1.3)
coercible (1.0.0)
@@ -607,7 +607,7 @@ GEM
rswag-ui (2.16.0)
actionpack (>= 5.2, < 8.1)
railties (>= 5.2, < 8.1)
rubocop (1.80.1)
rubocop (1.80.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)

View File

@@ -1,13 +1,7 @@
.crop-icon {
height: 1em;
}
.card-footer {
.btn-group-vertical {
.btn {
text-wrap: initial
}
}
}
.crop-thumbnail {
.text {
bottom: 0;

View File

@@ -1,8 +1,6 @@
// stats shown on homepage. eg. "999 members..."
.stats {
a {
font-weight: bold;
}
font-weight: bold;
}
.crops,

View File

@@ -10,33 +10,9 @@
width: 100%;
}
#navbarSupportedContent {
ul {
flex-direction: column-reverse;
flex-wrap: nowrap;
li.nav-item {
display: block;
a {
display: grid;
grid-template-columns: 2em 1fr 2em;
}
a.dropdown-toggle::after {
width: 100%;
text-align: right;
}
}
}
}
.crop-actions {
flex-direction: column;
width: 100%;
a {
margin: auto;
.navbar .nav > li {
display: block;
}
}
.navbar .navbar-form {
padding-left: 0;

View File

@@ -10,7 +10,6 @@ body {
.navbar {
flex-wrap: nowrap;
align-items: flex-start
}
.navbar-brand {
.site-name {
@@ -368,6 +367,9 @@ ul.thumbnail-buttons {
h1 {
font-size: 400%;
}
.stats a {
color: $black;
}
// signup widget on homepage
.signup {

View File

@@ -39,6 +39,12 @@ class CropsController < ApplicationController
respond_with @crops
end
def openfarm
@crop = Crop.find(params[:crop_slug])
@crop.update_openfarm_data!
respond_with @crop, location: @crop
end
def gbif
@crop = Crop.find(params[:crop_slug])
@crop.update_gbif_data!
@@ -131,6 +137,7 @@ class CropsController < ApplicationController
if @crop.approval_status_changed?(from: "pending", to: "approved")
notifier.deliver_now!
@crop.update_openfarm_data!
@crop.update_gbif_data!
end
else

View File

@@ -4,7 +4,7 @@ class GardensController < DataController
def index
@owner = Member.find_by(slug: params[:member_slug])
@show_all = params[:all] == '1'
@show_jump_to = params[:member_slug].present? || false
@show_jump_to = params[:member_slug].present? ? true : false
@gardens = @gardens.includes(:owner)
@gardens = @gardens.active unless @show_all
@@ -18,7 +18,7 @@ class GardensController < DataController
end
def show
@current_plantings = @garden.plantings.current.where.not(failed: true).includes(:crop, :owner).order(planted_at: :desc)
@current_plantings = @garden.plantings.current.includes(:crop, :owner).order(planted_at: :desc)
@current_activities = @garden.activities.current.includes(:owner).order(created_at: :desc)
@finished_plantings = @garden.plantings.finished.includes(:crop)
@suggested_companions = Crop.approved.where(

View File

@@ -19,7 +19,9 @@ class SeedsController < DataController
where['parent_planting'] = @planting.id
end
where['tradeable_to'] = params[:tradeable_to] if params[:tradeable_to].present?
if params[:tradeable_to].present?
where['tradeable_to'] = params[:tradeable_to]
end
@show_all = (params[:all] == '1')
where['finished'] = false unless @show_all
@@ -56,7 +58,6 @@ class SeedsController < DataController
def create
@seed = Seed.new(seed_params)
@seed.finished ||= false
@seed.owner = current_member
@seed.crop = @seed.parent_planting.crop if @seed.parent_planting
flash[:notice] = "Successfully added #{@seed.crop} seed to your stash." if @seed.save

View File

@@ -21,10 +21,6 @@ module ApplicationHelper
classes
end
def count_github_contibutors
File.open(Rails.root.join('CONTRIBUTORS.md')).readlines.grep(/^-/).size
end
# Produces a cache key for uniquely identifying cached fragments.
def cache_key_for(klass, identifier = "all")
count = klass.count

View File

@@ -53,7 +53,7 @@ module ButtonsHelper
link_to t('buttons.mark_as_inactive'),
garden_path(garden, garden: { active: 0 }),
method: :put, class: classes,
data: { confirm: I18n.t('gardens.confirm_deactivate') }
data: { confirm: 'All plantings associated with this garden will be marked as finished. Are you sure?' }
end
def create_button(model_to_create, path, icon, label)

View File

@@ -4,6 +4,10 @@ module OpenFarmData
extend ActiveSupport::Concern
included do
def update_openfarm_data!
OpenfarmService.new.update_crop(self)
end
def of_photo
fetch_attr('main_image_path')
end
@@ -39,6 +43,10 @@ module OpenFarmData
fetch_attr('common_names')
end
def guides_count
fetch_attr('guides_count')
end
def binomial_name
fetch_attr('binomial_name')
end

View File

@@ -94,9 +94,9 @@ module PredictPlanting
private
def calculate_percentage_grown
return 0 if age_in_days.to_i < 0
return 0 if age_in_days < 0
percent = (age_in_days.to_f / expected_lifespan.to_f) * 100
percent = (age_in_days / expected_lifespan.to_f) * 100
(percent > 100 ? 100 : percent)
end
end

View File

@@ -59,8 +59,7 @@ module SearchSeeds
search('*', limit:,
where: {
finished: false,
tradable: true,
_or: [{ plant_before: nil }, { plant_before: { lt: Date.today } }]
tradable: true
},
boost_by: [:created_at],
load: false)

View File

@@ -59,7 +59,6 @@ class Seed < ApplicationRecord
scope :has_location, -> { joins(:owner).where.not('members.location': nil) }
scope :recent, -> { order(created_at: :desc) }
scope :active, -> { where('finished <> true').where('finished_at IS NULL OR finished_at < ?', Time.zone.now) }
scope :expired, -> { active.where('plant_before < ?', Time.zone.today) }
def tradable
tradable_to != 'nowhere'

View File

@@ -1,6 +1,6 @@
- if crop.approved? && signed_in?
.btn-group.crop-actions{"aria-label" => "Crop Actions", role: "group"}
.btn-group{"aria-label" => "Crop Actions", role: "group"}
= render 'plantings/modal', planting: Planting.new(crop: crop, owner: current_member)
= render 'harvests/modal', harvest: Harvest.new(crop: @crop, owner: current_member)
= render 'seeds/modal', seed: Seed.new(crop: @crop, owner: current_member)

View File

@@ -25,9 +25,9 @@
Last harvest expected
%strong= crop.median_days_to_last_harvest
days after planting
- if member_signed_in?
.card-footer
.d-flex.btn-group-vertical
= render 'plantings/modal', planting: Planting.new(crop: crop, owner: current_member)
- #= render 'harvests/modal', harvest: Harvest.new(crop: crop, owner: current_member)
= render 'seeds/modal', seed: Seed.new(crop: crop, owner: current_member)
- if member_signed_in?
.card-footer
.d-flex.justify-content-between
= render 'plantings/modal', planting: Planting.new(crop: crop, owner: current_member)
- #= render 'harvests/modal', harvest: Harvest.new(crop: crop, owner: current_member)
= render 'seeds/modal', seed: Seed.new(crop: crop, owner: current_member)

View File

@@ -0,0 +1,6 @@
- if crop.guides_count.present? && crop.guides_count.positive?
%p
There are
= link_to "https://openfarm.cc/en/crops/#{CGI.escape @crop.name.gsub(' ', '-').downcase}" do
#{crop.guides_count} growing guides on Open Farm

View File

@@ -10,6 +10,10 @@
= edit_icon
= t('.edit')
= link_to crop_openfarm_path(crop), method: :post, class: 'dropdown-item' do
= icon 'far', 'update'
Fetch data from OpenFarm
= link_to crop_gbif_path(crop), method: :post, class: 'dropdown-item' do
= icon 'far', 'update'
Fetch data from GBIF

View File

@@ -74,6 +74,7 @@
.card-body
%h4 How to grow #{@crop.name.pluralize}
= render 'grown_for', crop: @crop
= render 'planting_advice', crop: @crop
- if @crop.parent
%hr/
%p.parent-crop
@@ -123,6 +124,13 @@
= icon 'fas', 'external-link-alt'
Wikipedia (English)
%li.list-group-item
= link_to "https://openfarm.cc/en/crops/#{CGI.escape @crop.name.gsub(' ', '-')}",
class: 'card-link',
target: "_blank",
rel: "noopener noreferrer" do
= icon 'fas', 'external-link-alt'
OpenFarm - Growing guide
%li.list-group-item
= link_to "https://www.gardenate.com/plant/#{CGI.escape @crop.name}",
target: "_blank",
@@ -139,14 +147,6 @@
= icon 'fas', 'external-link-alt'
Google
%li.list-group-item
= link_to 'https://chat.openai.com/?model=gpt-4o&prompt=' + CGI.escape(['How do I grow', @crop.name, "and what grows well with it? What should I plant the next season if practicing crop rotation? Explain why and add links to your sources"].join(' ')),
target: "_blank",
class: 'card-link',
rel: "noopener noreferrer" do
= icon 'fas', 'external-link-alt'
ChatGPT
%li.list-group-item
= link_to "https://wikihow.com/wikiHowTo?search=#{CGI.escape "grow #{@crop.name}" }",
target: "_blank",

View File

@@ -15,4 +15,5 @@
- if can?(:destroy, garden)
.dropdown-divider
= delete_button(garden, classes: 'dropdown-item text-danger', message: 'gardens.confirm_delete')
= delete_button(garden, classes: 'dropdown-item text-danger',
message: 'All plantings associated with this garden will also be deleted. Are you sure?')

View File

@@ -66,7 +66,8 @@
- if can?(:destroy, @garden)
.dropdown-divider
= delete_button(@garden, classes: 'dropdown-item text-danger', message: 'gardens.confirm_delete')
= delete_button(@garden, classes: 'dropdown-item text-danger',
message: 'All plantings associated with this garden will also be deleted. Are you sure?')
%section
%h2 Current activities in garden

View File

@@ -4,6 +4,6 @@
member: link_to(t('.member_linktext', count: Member.confirmed.size.to_i), members_path),
number_crops: link_to(t('.number_crops_linktext', count: Crop.count.to_i), crops_path),
number_plantings: link_to(t('.number_plantings_linktext', count: Planting.count.to_i), plantings_path),
number_gardens: link_to(t('.number_gardens_linktext', count: Garden.count.to_i), gardens_path),
contributors: link_to(count_github_contibutors, 'http://github.com/Growstuff/growstuff/CONTRIBUTORS.md', target: '_blank', rel: 'noopener'),
github: link_to('GitHub', 'http://github.com/Growstuff/growstuff', target: '_blank', rel: 'noopener'))
number_gardens: link_to(t('.number_gardens_linktext', count: Garden.count.to_i), gardens_path))

View File

@@ -8,7 +8,6 @@
%p= render 'stats', cached: true
.col
%br
%p
- if current_member.plantings.active.any?
= link_to member_path(current_member, anchor: "#content"), class: 'btn btn-dark' do
@@ -62,16 +61,3 @@
%section.members
= cute_icon
= render 'members', cached: true
.col-12.col-lg-6
%section.pwa-install
= cute_icon
%h2.text-center= t('home.pwa_title')
.index-cards
.card
.card-body
%h3= t('home.pwa_ios_title')
%p= t('home.pwa_ios_steps_html')
.card
.card-body
%h3= t('home.pwa_android_title')
%p= t('home.pwa_android_steps_html')

View File

@@ -6,6 +6,6 @@
%span.site-name Growstuff
.nav= render 'crops/search_bar'
.nav
%button.navbar-toggler.ml-auto{ "aria-controls" => "navbarSupportedContent", "aria-expanded" => "false", "aria-label" => "Toggle navigation", "data-bs-target" => "#navbarSupportedContent", "data-bs-toggle" => "collapse", type: "button" }
%span.navbar-toggler-icon
%button.navbar-toggler{ "aria-controls" => "navbarSupportedContent", "aria-expanded" => "false", "aria-label" => "Toggle navigation", "data-bs-target" => "#navbarSupportedContent", "data-bs-toggle" => "collapse", type: "button" }
%i.fas.fa-ellipsis-v.navbar-toggler-icon
= render 'layouts/menu'

View File

@@ -1,5 +1,5 @@
#navbarSupportedContent.collapse.navbar-collapse
%ul.navbar-nav.mr-auto.bg-dark
%ul.navbar-nav.mr-auto
- if signed_in?
%li.nav-item
= link_to timeline_index_path, method: :get, class: 'nav-link text-white' do
@@ -30,9 +30,7 @@
- cache("everyone-menu", expires_in: 1.week) do
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}
%span
= t('.crops')
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}= t('.crops')
.dropdown-menu
= link_to crops_path, class: 'dropdown-item' do
= t('.browse_crops')
@@ -46,9 +44,7 @@
= harvest_icon
= t('.harvests')
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}
%span
= t('.community')
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}= t('.community')
.dropdown-menu{"aria-labelledby" => "navbarDropdown"}
= link_to t('.community_map'), places_path, class: 'dropdown-item'
= link_to t('.browse_members'), members_path, class: 'dropdown-item'
@@ -58,9 +54,7 @@
- if member_signed_in?
- if current_member.role?(:crop_wrangler) || current_member.role?(:admin)
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}
%span
= t('.admin')
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}= t('.admin')
.dropdown-menu{"aria-labelledby" => "navbarDropdown"}
- if current_member.role?(:crop_wrangler)
= link_to t('.crop_wrangling'), wrangle_crops_path, class: 'dropdown-item'

View File

@@ -1,5 +1,5 @@
- cache member do
.card
.card.card-double
.card-body
%h4.login-name= link_to member, member
%div

View File

@@ -82,8 +82,8 @@
.col-md-10#content
.row
%section.order-3.order-md-1.col-12= render "map", member: @member
- if @harvesting.size.positive?
%section.harvests.order-2.order-md-1.col-12
- if @harvesting.size.positive?
%section.harvests.order-2.order-md-1
%h2 Ready to harvest
.index-cards
- @harvesting.each do |planting|

View File

@@ -21,7 +21,7 @@
- if planting.finish_is_predicatable?
.card.fact-card
%h3 Progress
- if planting.age_in_days.to_i < 0
- if planting.age_in_days < 0
%strong Planned
- else
%strong #{planting.age_in_days}/#{planting.expected_lifespan}
@@ -45,7 +45,7 @@
%h3 Growing
%strong= seedling_icon
%span
- if planting.age_in_days.to_i < 0
- if planting.age_in_days < 0
Planting planned
- else
Planting is still growing today

View File

@@ -39,10 +39,10 @@
- elsif @planting.percentage_grown.present?
#{@planting.percentage_grown.to_i}%
- if @planting.finish_is_predicatable?
- if @planting.age_in_days.to_i < 0
- if @planting.age_in_days < 0
%strong Planned
- else
%strong #{@planting.age_in_days.to_i}/#{@planting.expected_lifespan} days
%strong #{@planting.age_in_days}/#{@planting.expected_lifespan} days
= render 'timeline', planting: @planting
= render 'likes/likes', object: @planting

View File

@@ -16,20 +16,14 @@
%p
- if seed.quantity
.badge.badge-info #{seed.quantity} seeds
%ul
- if seed.organic != 'unknown'
%li
%small.seedtitle--organic= seed.organic
- if seed.gmo != 'unknown'
%li
%small.seedtitle--gmo= seed.gmo
- if seed.heirloom != 'unknown'
%li
%small.seedtitle--heirloom= seed.heirloom
.card-footer
.d-flex.w-100.justify-content-between
- if seed.tradable
- if seed.organic != 'unknown'
.badge.badge-success.seedtitle--organic= seed.organic
- if seed.gmo != 'unknown'
.badge.badge-success.seedtitle--gmo= seed.gmo
- if seed.heirloom != 'unknown'
.badge.badge-success.seedtitle--heirloom= seed.heirloom
- if seed.tradable
.card-footer
.d-flex.w-100.justify-content-between
%small Will trade #{seed.tradable_to}
- if seed.plant_before
%small Plant before #{seed.plant_before}
/ %a.btn.btn-sm{href: "#"} Request

View File

@@ -123,8 +123,6 @@ en:
no_plantings: no plantings
plantingsthumbnail: plantings/thumbnail
updated: Garden was successfully updated.
confirm_delete: All plantings associated with this garden will also be deleted. Are you sure?
confirm_deactivate: All plantings associated with this garden will be marked as finished. Are you sure?
harvests:
created: Harvest was successfully created.
harvest_something: Harvest something
@@ -177,6 +175,7 @@ en:
Our team includes volunteers from all walks of life and all skill levels. To get involved,
visit %{talk_link} or find more information on the %{wiki_link}.
get_involved_title: Get Involved
github_linktext: Github
open_data_body_html: >
We're building a database of crops, planting advice, seed sources, and other information that anyone
can use for free, under a %{creative_commons_link}. You can use this data for research, to build apps,
@@ -208,15 +207,10 @@ en:
view_all: View all seeds
stats:
member_linktext: "%{count} members"
message_html: So far, %{member} have planted %{number_crops} %{number_plantings} in %{number_gardens}; and %{contributors} people have contributed to our code on %{github}!
message_html: So far, %{member} have planted %{number_crops} %{number_plantings} in %{number_gardens}.
number_crops_linktext: "%{count} crops"
number_gardens_linktext: "%{count} gardens"
number_plantings_linktext: "%{count} times"
pwa_android_steps_html: 1. Tap the three dots in the top right corner of Chrome.<br>2. Tap <strong>Install app</strong> or <strong>Add to Home screen</strong>.
pwa_android_title: For Android
pwa_ios_steps_html: 1. Tap the <strong>Share</strong> button in Safari.<br>2. Scroll down and tap <strong>Add to Home Screen</strong>'.
pwa_ios_title: For iOS (iPhone/iPad)
pwa_title: Want to install Growstuff on your phone?
label:
days_until_harvest: "%{number} days"
weeks_until_harvest: "%{number} weeks until harvest"

View File

@@ -81,6 +81,7 @@ Rails.application.routes.draw do
get 'sunniness' => 'charts/crops#sunniness', constraints: { format: 'json' }
get 'planted_from' => 'charts/crops#planted_from', constraints: { format: 'json' }
get 'harvested_for' => 'charts/crops#harvested_for', constraints: { format: 'json' }
post :openfarm
post :gbif
collection do

View File

@@ -2,6 +2,6 @@
class AddLanguageToAlternateNames < ActiveRecord::Migration[7.2]
def change
add_column :alternate_names, :language, :string
add_column :alternate_names, :language, :string, null: false
end
end

View File

@@ -173,8 +173,7 @@ def load_test_users
organic: select_random_item(Seed::ORGANIC_VALUES),
gmo: select_random_item(['certified GMO-free', 'non-certified GMO-free', 'GMO', 'unknown']), # Strangely, this doesn't want to work as Seed:GMO_VALUES
heirloom: select_random_item(Seed::HEIRLOOM_VALUES),
parent_planting: @user.plantings.first,
finished: false
parent_planting: @user.plantings.first
)
photo = Photo.create!(

View File

@@ -53,12 +53,4 @@ namespace :growstuff do
end
end
end
desc "Mark seeds as finished when plant-before date expires"
# usage: rake growstuff:finish_expired_seeds
task finish_expired_seeds: :environment do
Seed.expired.find_each do |seed|
seed.update(finished: true, finished_at: Time.zone.now)
end
end
end

View File

@@ -76,6 +76,10 @@ describe "crop detail page", :js do
expect(page).to have_link "Wikipedia (English)", href: crop.en_wikipedia_url
end
it "has a link to OpenFarm" do
expect(page).to have_link "OpenFarm - Growing guide"
end
it "has a link to gardenate" do
expect(page).to have_link "Gardenate - Planting reminders",
href: "https://www.gardenate.com/plant/#{CGI.escape crop.name}"
@@ -140,7 +144,7 @@ describe "crop detail page", :js do
let(:crop) { FactoryBot.create(:annual_crop) }
describe 'with harvests' do
it_behaves_like "predicts harvest"
include_examples "predicts harvest"
end
it "predicts lifespan" do
@@ -161,7 +165,7 @@ describe "crop detail page", :js do
end
describe 'with harvests' do
it_behaves_like "predicts harvest"
include_examples "predicts harvest"
end
it "describes Perennial crops" do
@@ -177,7 +181,7 @@ describe "crop detail page", :js do
end
describe 'with harvests' do
it_behaves_like "predicts harvest"
include_examples "predicts harvest"
end
end
end

View File

@@ -85,22 +85,21 @@ describe "home page", :search do
end
context 'when anonymous' do
it_behaves_like 'show crops'
it_behaves_like 'show plantings'
it_behaves_like 'show harvests'
it_behaves_like 'shows seeds'
it_behaves_like 'is accessible'
include_examples 'show crops'
include_examples 'show plantings'
include_examples 'show harvests'
include_examples 'shows seeds'
include_examples 'is accessible'
it { is_expected.to have_text 'community of food gardeners' }
it { is_expected.to have_text 'Want to install Growstuff on your phone?' }
end
context "when signed in" do
include_context 'signed in member'
it_behaves_like 'show crops'
it_behaves_like 'show plantings'
it_behaves_like 'show harvests'
it_behaves_like 'shows seeds'
it_behaves_like 'is accessible'
include_examples 'show crops'
include_examples 'show plantings'
include_examples 'show harvests'
include_examples 'shows seeds'
include_examples 'is accessible'
describe 'should say welcome' do
before { visit root_path }

View File

@@ -132,20 +132,6 @@ describe Seed do
end
end
context 'expired' do
it 'returns seeds with a plant_before date in the past' do
expired_seed = FactoryBot.create(:seed, plant_before: 1.day.ago)
not_expired_seed = FactoryBot.create(:seed, plant_before: 1.day.from_now)
described_class.expired.should include expired_seed
described_class.expired.should_not include not_expired_seed
end
it 'does not return finished seeds' do
expired_seed = FactoryBot.create(:seed, plant_before: 1.day.ago, finished: true)
described_class.expired.should_not include expired_seed
end
end
context 'interesting' do
it 'lists interesting seeds' do
# to be interesting a seed must:

View File

@@ -14,7 +14,7 @@ RSpec.configure do |config|
# By default, the operations defined in spec files are added to the first
# document below. You can override this behavior by adding a swagger_doc tag to the
# the root example_group in your specs, e.g. describe '...', swagger_doc: 'v2/swagger.json'
config.swagger_docs = {
config.openapi_specs = {
'v1/swagger.yaml' => {
openapi: '3.0.1',
info: {
@@ -29,5 +29,5 @@ RSpec.configure do |config|
# The swagger_docs configuration option has the filename including format in
# the key, this may want to be changed to avoid putting yaml in json files.
# Defaults to json. Accepts ':json' and ':yaml'.
config.swagger_format = :yaml
config.openapi_format = :yaml
end

6
swagger/v1/swagger.yaml Normal file
View File

@@ -0,0 +1,6 @@
---
openapi: 3.0.1
info:
title: API V1
version: v1
paths: {}