Enhance crop lifespan and job templates display with new styles and functionality

This commit is contained in:
Brenda Wallace committed 2026-11-04 06:31:14 +13:00
1 parent 85fe7dde15
commit ea46d88d3c
8 files changed
+191 -50

No files matched your search

+9
View File
@@ -229,6 +229,15 @@
}
}
// Annual or perennial is the point of the lifespan card, so it is the biggest thing in it.
.lifespan-word {
display: block;
font-size: 2rem;
font-weight: 700;
line-height: 1.2;
margin-bottom: 0.25rem;
}
// An annual's median lifespan, under the word annual.
.crop-median-lifespan {
align-items: center;
+15 -21
View File
@@ -1107,38 +1107,32 @@ $notepad-trail: #a4c98c;
}
.crop-notepad {
background: $white;
border: 1px solid rgba(65, 63, 59, 0.15);
border-radius: $card-radius;
box-shadow: 1px 3px 3px 1px #cac1b3;
margin-bottom: 1rem;
overflow: hidden;
padding-bottom: 0.5rem;
}
// Only shown to tell several templates apart, or to say a template is for part of the
// year, so it is a subheading, not a second bar inside the section's.
.crop-notepad-title {
align-items: baseline;
background: $green;
display: flex;
gap: 1rem;
justify-content: space-between;
padding: 0.5rem 1.1rem;
margin-bottom: 0.25rem;
// The site makes headings in a section green banners with a shadow; this one
// is plain text on the notepad's own bar.
// The site makes headings in a section green banners with a shadow; this one is plain.
h3 {
background: none;
box-shadow: none;
color: $white;
font-size: 1.3rem;
font-weight: 400;
color: $notepad-leaf;
font-size: 1.1rem;
font-weight: 700;
margin: 0;
padding: 0;
}
}
.crop-notepad-season {
color: $white;
color: $ink-muted;
font-size: 0.85rem;
}
@@ -1203,13 +1197,13 @@ $notepad-trail: #a4c98c;
.crop-job-add {
margin: 0.25rem 0 1rem;
// White on the button, as in the heading bars, not the menu's green.
> .menu-icon,
.btn > .menu-icon {
color: inherit;
margin-right: 0.4rem;
vertical-align: text-bottom;
}
}
// White on the button, as in the heading bars, not the menu's green.
.record-page .crop-job-add .menu-icon {
color: inherit;
margin-right: 0.4rem;
vertical-align: text-bottom;
}
// The day to plant on, above the button that adds the jobs.
+12
View File
@@ -189,6 +189,18 @@ class Crop < ApplicationRecord
reason_for_rejection
end
# The seed packets of this crop and of its varieties that give a figure for a
# packet fact such as row_spacing. A packet that leaves it blank, or says zero,
# gives none.
def seeds_with(attribute)
Seed.of_crop(self).where.not(attribute => [nil, 0])
end
# The median of that figure across those packets, or nil when there are none.
def seed_median(attribute)
seeds_with(attribute).median(attribute)
end
def update_medians
plantings.each(&:update_harvest_days!)
update_lifespan_medians
+7 -4
View File
@@ -6,10 +6,13 @@
.crop-job-templates
- templates.each do |template|
.crop-notepad
.crop-notepad-title
%h3= template.name
- unless template.season_any?
%span.crop-notepad-season= crop_job_option(:season, template.season)
-# The section's heading already says what this is: a template's own name is
-# only needed to tell several apart, or to say it is for part of the year.
- if templates.size > 1 || !template.season_any?
.crop-notepad-title
%h3= template.name
- unless template.season_any?
%span.crop-notepad-season= crop_job_option(:season, template.season)
%ol.crop-notepad-steps
- template.steps.each do |step|
%li.crop-job-step
+24 -21
View File
@@ -11,26 +11,24 @@
.card.fact-card.lifespan-card
.card-body.text-center
%h3
= link_to 'https://en.wikipedia.org/wiki/Annual_vs._perennial_plant_evolution', class: 'crop-longevity' do
Lifespan
= editable :select, crop, :perennial, display_field: '.lifespan-display', values: [['Annual', false], ['Perennial', true]]
.lifespan-display
= link_to 'https://en.wikipedia.org/wiki/Annual_vs._perennial_plant_evolution', class: 'lifespan-word' do
= crop.perennial? ? 'Perennial' : 'Annual'
- if crop.perennial?
%small living more than two years
- else
%small living and reproducing in a single year or less
- if crop.median_lifespan.present?
- weeks = in_weeks(crop.median_lifespan)
.crop-median-lifespan
%i.far.fa-calendar.fa-2x.amber-text{ 'aria-hidden': 'true' }
- if weeks > 1
%strong #{weeks} #{'week'.pluralize(weeks)}
%small median lifespan
- else
%small.text-muted We don't have enough data yet on the median lifespan.
- if can? :wrangle, @crop
%small.edit-link
= bootstrap_form_for(@crop) do |f|
= f.hidden_field :perennial, value: crop.annual?
= f.submit :toggle
- if crop.perennial?
%small living more than two years
- else
%small living and reproducing in a single year or less
- if crop.median_lifespan.present?
- weeks = in_weeks(crop.median_lifespan)
.crop-median-lifespan
%i.far.fa-calendar.fa-2x.amber-text{ 'aria-hidden': 'true' }
- if weeks > 1
%strong #{weeks} #{'week'.pluralize(weeks)}
%small median lifespan
- else
%small.text-muted We don't have enough data yet on the median lifespan.
- if crop.median_days_to_first_harvest.present?
= render 'layouts/fact_card',
@@ -48,9 +46,14 @@
- if crop.spread.present?
= render 'layouts/fact_card',
title: 'Spread', value: "#{crop.spread}cm", description: nil
- if crop.row_spacing.present?
-# Members' seed packets say what the crop needs, if the crop itself doesn't: their median.
- packets_spacing = crop.seed_median(:row_spacing)
- if packets_spacing || crop.row_spacing.present?
- spacing = packets_spacing || crop.row_spacing
- packets = crop.seeds_with(:row_spacing).count if packets_spacing
= render 'layouts/fact_card',
title: 'Row Spacing', value: "#{crop.row_spacing}cm", description: nil
title: 'Row Spacing', value: "#{number_with_precision(spacing, precision: 1, strip_insignificant_zeros: true)}cm",
description: (packets ? "median of #{pluralize(packets, 'seed packet')}" : nil)
- if crop.growing_degree_days.present?
= render 'layouts/fact_card',
title: 'Growing Degree Days', value: crop.growing_degree_days, description: nil
+52
View File
@@ -707,4 +707,56 @@ describe Crop do
expect(described_class.icon_names).to eq described_class.icon_names.sort
end
end
describe 'facts from seed packets' do
subject(:crop) { create(:tomato) }
let(:variety) { create(:crop, name: 'cherry tomato', parent: crop) }
describe '#seed_median' do
it { expect(crop.seed_median(:row_spacing)).to be_nil }
context 'with packets that give a row spacing' do
before do
create(:seed, crop:, row_spacing: 30)
create(:seed, crop:, row_spacing: 60)
create(:seed, crop:, row_spacing: 90)
end
it { expect(crop.seed_median(:row_spacing)).to eq 60 }
end
context 'with an even number of packets' do
before do
create(:seed, crop:, row_spacing: 30)
create(:seed, crop:, row_spacing: 50)
end
it { expect(crop.seed_median(:row_spacing)).to eq 40 }
end
context 'with a packet that leaves it blank, or says zero' do
before do
create(:seed, crop:, row_spacing: 30)
create(:seed, crop:, row_spacing: nil)
create(:seed, crop:).update_column(:row_spacing, 0) # rubocop:disable Rails/SkipsModelValidations
end
it { expect(crop.seed_median(:row_spacing)).to eq 30 }
it { expect(crop.seeds_with(:row_spacing).count).to eq 1 }
end
context 'with a packet of a variety' do
before { create(:seed, crop: variety, row_spacing: 45) }
it { expect(crop.seed_median(:row_spacing)).to eq 45 }
end
context 'with a packet of another crop' do
before { create(:seed, crop: create(:crop, name: 'carrot'), row_spacing: 10) }
it { expect(crop.seed_median(:row_spacing)).to be_nil }
end
end
end
end
@@ -11,7 +11,7 @@ describe CropsController, '#show job templates' do
before { get crop_path(crop) }
it { is_expected.to have_css('.crop-job-templates h3', text: 'Tomato jobs') }
it { is_expected.to have_no_css('.crop-job-templates h3') }
it { is_expected.to have_css('.crop-job-step', text: 'Stake or cage the plants') }
it { is_expected.to have_css('.crop-job-step', text: '2 weeks after planting out') }
@@ -95,6 +95,23 @@ describe CropsController, '#show job templates' do
it { is_expected.to have_no_css('.crop-job-add') }
end
context 'with two templates' do
before do
create(:crop_job_step, crop_job_template: create(:crop_job_template, crop:, name: 'Early jobs', status: 'published'))
get crop_path(crop)
end
it { is_expected.to have_css('.crop-job-templates h3', text: 'Early jobs') }
it { is_expected.to have_css('.crop-job-templates h3', text: 'Tomato jobs') }
end
context 'with a template for part of the year' do
let(:template) { create(:crop_job_template, crop:, name: 'Tomato jobs', status: 'published', season: 'spring_summer') }
it { is_expected.to have_css('.crop-job-templates h3', text: 'Tomato jobs') }
it { is_expected.to have_css('.crop-notepad-season', text: 'Spring and summer') }
end
context 'with a draft template' do
let(:template) { create(:crop_job_template, crop:, name: 'Tomato jobs', status: 'draft') }
@@ -106,7 +123,7 @@ describe CropsController, '#show job templates' do
before { get crop_path(variety) }
it { is_expected.to have_css('.crop-job-templates h3', text: 'Tomato jobs') }
it { is_expected.to have_no_css('.crop-job-templates h3') }
end
context 'with a template for another crop' do
@@ -89,7 +89,7 @@ describe CropsController, '#show sections' do
context 'with an annual with a median lifespan of 13 weeks' do
let(:perennial) { false }
it { is_expected.to have_css('.lifespan-card h3', text: 'Annual') }
it { is_expected.to have_css('.lifespan-card .lifespan-word', text: 'Annual') }
it { is_expected.to have_css('.lifespan-card .crop-median-lifespan', text: '13 weeks median lifespan') }
it { is_expected.to have_css('.lifespan-card .fa-calendar') }
it { is_expected.to have_no_css('.lifespan-card svg') }
@@ -113,7 +113,7 @@ describe CropsController, '#show sections' do
context 'with a perennial' do
let(:perennial) { true }
it { is_expected.to have_css('.lifespan-card h3', text: 'Perennial') }
it { is_expected.to have_css('.lifespan-card .lifespan-word', text: 'Perennial') }
it { is_expected.to have_no_css('.crop-median-lifespan') }
end
end
@@ -134,4 +134,55 @@ describe CropsController, '#show sections' do
it { is_expected.to have_no_css('section.crop-jobs') }
end
describe 'changing whether the crop is annual or perennial' do
context 'when signed in as a crop wrangler' do
before do
sign_in create(:crop_wrangling_member)
get crop_path(crop)
end
it { is_expected.to have_css('.lifespan-card .edit-link a.editable') }
it { is_expected.to have_css('.lifespan-card select[name="crop[perennial]"]', visible: :all) }
it { is_expected.to have_no_css('.lifespan-card input[type="submit"][value="toggle"]', visible: :all) }
end
context 'when signed in as an ordinary member' do
before do
sign_in create(:member)
get crop_path(crop)
end
it { is_expected.to have_no_css('.lifespan-card .edit-link') }
it { is_expected.to have_no_css('.lifespan-card select', visible: :all) }
end
end
describe 'the row spacing card' do
context 'with seed packets that give a row spacing' do
before do
create(:seed, crop:, row_spacing: 30)
create(:seed, crop:, row_spacing: 60)
create(:seed, crop:, row_spacing: nil)
get crop_path(crop)
end
it { is_expected.to have_css('.fact-card', text: 'Row Spacing 45cm median of 2 seed packets') }
end
context 'with a crop row spacing and no packets that give one' do
let(:crop) { create(:tomato, row_spacing: 25) }
before { get crop_path(crop) }
it { is_expected.to have_css('.fact-card', text: 'Row Spacing 25cm') }
it { is_expected.to have_no_css('.fact-card', text: 'median of') }
end
context 'with neither' do
before { get crop_path(crop) }
it { is_expected.to have_no_css('.fact-card', text: 'Row Spacing') }
end
end
end