mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-11-05 12:42:48 -05:00
What a packet says about sowing it. When to sow is the months, as many as the packet says: a form sends twelve chips to tick, with the seasons as shortcuts for the grower's hemisphere (none where we do not know it, rather than a guess), and the list shows them as runs like "Oct-Feb". How to sow is text, in the packet's words. Germination is a range of days, as maturity is. A migration adds four columns to seeds. The facts are on the packet sheet, in the order they happen, and editable from the pencils, from Edit all and from the standalone form. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
243 lines
9.1 KiB
Ruby
243 lines
9.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class SeedsController < DataController
|
|
GROUPINGS = %w(crop source year heirloom).freeze
|
|
PER_PAGE = 30 # piles of packets on a page of the web page, seeds in the data formats
|
|
SEED_LIBRARY_HEADER = 'X-Seed-Library'
|
|
|
|
def index
|
|
@seeds = Seed.all
|
|
|
|
if params[:member_slug].present?
|
|
@owner = Member.find_by!(slug: params[:member_slug])
|
|
@seeds = @seeds.where(owner_id: @owner.id)
|
|
end
|
|
|
|
if params[:crop_slug].present?
|
|
@crop = Crop.find_by(slug: params[:crop_slug])
|
|
@seeds = @seeds.where(crop_id: @crop.id) if @crop
|
|
end
|
|
|
|
if params[:planting_id].present?
|
|
@planting = Planting.find_by(slug: params[:planting_id])
|
|
@seeds = @seeds.where(parent_planting_id: @planting.id) if @planting
|
|
end
|
|
|
|
@seeds = filter(@seeds)
|
|
@filtered = %i(from within tradeable_to past).any? { |name| params[name].present? }
|
|
|
|
# Searching is by choosing a crop, so there is only ever a real one to offer.
|
|
@search_crop = Crop.find_by(slug: params[:crop]) if params[:crop].present? && @crop.blank?
|
|
@seeds = @seeds.of_crop(@search_crop) if @search_crop
|
|
|
|
@owned_crop_ids = current_member ? current_member.seeds.current.pluck(:crop_id) : []
|
|
@past_count = @seeds.current.expired.count
|
|
@seeds = @seeds.expired if params[:past] == '1'
|
|
|
|
@in_stock_count = @seeds.current.count
|
|
@finished_count = @seeds.finished.count
|
|
@show_finished = (params[:finished] == '1')
|
|
@other_count = @show_finished ? @in_stock_count : @finished_count
|
|
@show_all = (params[:all] == '1')
|
|
@seeds = if @show_all then @seeds
|
|
elsif @show_finished then @seeds.finished
|
|
else @seeds.current
|
|
end
|
|
@stats = library_stats(@owner) if @owner
|
|
@seed_total = @seeds.count
|
|
@shared_totals = { packets: @seed_total, gardeners: @seeds.distinct.count(:owner_id) } if @everyone
|
|
|
|
@filename = csv_filename
|
|
@sort_by_distance = @crop.present? && current_member&.latitude.present? && current_member&.longitude.present?
|
|
@seeds = @seeds.includes({ crop: :parent }, :owner)
|
|
@seeds = @sort_by_distance ? order_by_distance(@seeds, current_member) : @seeds.order(created_at: :desc)
|
|
if page_of_piles?
|
|
@seeds, @piles = paginate_piles(@seeds)
|
|
else
|
|
@seeds = @seeds.paginate(page: params[:page], per_page: PER_PAGE)
|
|
end
|
|
@group_by = GROUPINGS.include?(params[:group]) ? params[:group] : 'crop'
|
|
@list_view = (params[:view] == 'list')
|
|
@shelves = shelve(@seeds, @group_by)
|
|
|
|
# The page and its results are the same address, so caches must tell them apart.
|
|
response.headers['Vary'] = SEED_LIBRARY_HEADER
|
|
return render_results if request.headers[SEED_LIBRARY_HEADER] == 'results'
|
|
|
|
respond_with(@seeds)
|
|
end
|
|
|
|
def show
|
|
@photos = @seed.photos.includes(:owner).order(created_at: :desc, id: :desc).paginate(page: params[:page], per_page: 30)
|
|
respond_with(@seed)
|
|
end
|
|
|
|
def new
|
|
@seed = Seed.new
|
|
@seed.source = 'my own seed saving'
|
|
|
|
if params[:planting_slug]
|
|
@planting = Planting.find_by(slug: params[:planting_slug])
|
|
else
|
|
@crop = Crop.find_or_initialize_by(id: params[:crop_id])
|
|
end
|
|
return render json: SeedFormSerializer.new(@planting, member: current_member, crop: @crop) if request.format.json? && (@planting || @crop&.persisted?)
|
|
|
|
respond_with(@seed)
|
|
end
|
|
|
|
def edit; end
|
|
|
|
def create
|
|
@seed = Seed.new(seed_params)
|
|
@seed.source ||= 'my own seed saving'
|
|
@seed.finished ||= false
|
|
@seed.owner = current_member
|
|
@seed.crop = @seed.parent_planting.crop if @seed.parent_planting
|
|
saved = @seed.save
|
|
return render_created_json if request.format.json?
|
|
|
|
flash[:notice] = t('seeds.added_to_stash', crop: @seed.crop) if saved
|
|
if params[:return] == 'planting'
|
|
respond_with(@seed, location: @seed.parent_planting)
|
|
else
|
|
respond_with(@seed)
|
|
end
|
|
end
|
|
|
|
def update
|
|
flash[:notice] = t('seeds.updated') if @seed.update(seed_params)
|
|
respond_with(@seed)
|
|
end
|
|
|
|
def destroy
|
|
@seed.destroy
|
|
respond_with(@seed)
|
|
end
|
|
|
|
private
|
|
|
|
# The web page, and the results the seed library fetches for it, come a page
|
|
# of piles at a time; the data formats come a page of seeds.
|
|
def page_of_piles?
|
|
request.format.html? || request.headers[SEED_LIBRARY_HEADER] == 'results'
|
|
end
|
|
|
|
# A page of whole piles: all of one gardener's packets of a crop come on the
|
|
# same page, never split across two. Returns that page's seeds, and the pager
|
|
# for its links. `seeds` carries the order they should come in.
|
|
def paginate_piles(seeds)
|
|
piles = seeds.reorder(nil).distinct.pluck(:owner_id, :crop_id)
|
|
crops = Crop.where(id: piles.map(&:last).uniq).includes(:parent).index_by(&:id)
|
|
piles.sort_by! { |owner_id, crop_id| [(crops[crop_id].parent || crops[crop_id]).name.downcase, crops[crop_id].name.downcase, owner_id] }
|
|
|
|
pager = WillPaginate::Collection.create(params[:page] || 1, PER_PAGE, piles.size) do |page|
|
|
page.replace(piles[page.offset, page.per_page] || [])
|
|
end
|
|
on_page = pager.to_set
|
|
page_seeds = seeds.where(owner_id: on_page.map(&:first), crop_id: on_page.map(&:last))
|
|
[page_seeds.select { |seed| on_page.include?([seed.owner_id, seed.crop_id]) }, pager]
|
|
end
|
|
|
|
# The seed library fetches its own controls' results alone, as the tabs, the
|
|
# results and the crop chosen, rather than the whole page.
|
|
def render_results
|
|
crop = { name: @search_crop.name, slug: @search_crop.slug } if @search_crop
|
|
render json: {
|
|
tabs: render_to_string(partial: 'seeds/tabs', formats: [:html]),
|
|
results: render_to_string(partial: 'seeds/results', formats: [:html]),
|
|
crop:
|
|
}
|
|
end
|
|
|
|
# The chips and scope switcher above the shelves. Everyone's seeds are the
|
|
# ones gardeners chose to share.
|
|
def filter(seeds)
|
|
@everyone = @owner.blank? && @planting.blank? && request.format.html?
|
|
seeds = seeds.tradable if @everyone
|
|
seeds = seeds.where(tradable_to: params[:tradeable_to]) if params[:tradeable_to].present?
|
|
seeds = seeds.from_source(params[:from]) if Seed::SOURCES_BY_KIND.key?(params[:from])
|
|
seeds = seeds.heirlooms if params[:from] == 'heirloom'
|
|
seeds = seeds.tradable if params[:from] == 'shared'
|
|
seeds = seeds.where(owner_id: scoped_owner_ids) if @everyone && current_member && %w(following near).include?(params[:scope])
|
|
seeds = seeds.where(owner_id: owner_ids_within(params[:within].to_i)) if @everyone && params[:within].to_i.positive?
|
|
seeds
|
|
end
|
|
|
|
def scoped_owner_ids
|
|
return current_member.followed.select(:id) if params[:scope] == 'following'
|
|
|
|
owner_ids_within(100)
|
|
end
|
|
|
|
def owner_ids_within(radius)
|
|
return [] unless current_member&.latitude
|
|
|
|
Member.located.near([current_member.latitude, current_member.longitude], radius, units: :km).reorder(nil).pluck(:id)
|
|
end
|
|
|
|
# The numbers at the top of a member's library.
|
|
def library_stats(owner)
|
|
seeds = Seed.where(owner_id: owner.id).current.includes(:crop)
|
|
crops = seeds.map(&:crop)
|
|
{
|
|
packets: seeds.size,
|
|
varieties: crops.uniq.size,
|
|
crops: crops.map { |crop| crop.parent || crop }.uniq.size,
|
|
own: seeds.count { |seed| seed.source == 'my own seed saving' }
|
|
}
|
|
end
|
|
|
|
# The page's seeds on shelves: a hash of shelf => seeds, where the shelf is
|
|
# the parent Crop when grouping by crop, otherwise the label to print.
|
|
def shelve(seeds, grouping)
|
|
shelves = seeds.group_by do |seed|
|
|
case grouping
|
|
when 'source' then seed.source.presence || 'other/unknown'
|
|
when 'year' then (seed.saved_at || seed.created_at).year.to_s
|
|
when 'heirloom' then seed.heirloom
|
|
else seed.crop.parent || seed.crop
|
|
end
|
|
end
|
|
shelves.sort_by { |shelf, _| grouping == 'year' ? -shelf.to_i : shelf.to_s.downcase }.to_h
|
|
end
|
|
|
|
# Nearest owners first; seeds whose owner has no location go last.
|
|
def order_by_distance(seeds, member)
|
|
distance = Geocoder::Sql.approx_distance(member.latitude, member.longitude,
|
|
'members.latitude', 'members.longitude')
|
|
seeds.order(Arel.sql("members.latitude IS NULL, #{distance} ASC, seeds.created_at DESC"))
|
|
end
|
|
|
|
# Used by the React garden cards, which stay on the page and say the seeds
|
|
# were saved (with a link to them). No flash: nothing redirects, so it would
|
|
# turn up on whichever page is loaded next.
|
|
def render_created_json
|
|
if @seed.persisted?
|
|
render json: { seed: { id: @seed.id, url: seed_path(@seed) } }, status: :created
|
|
else
|
|
render json: { errors: @seed.errors }, status: :unprocessable_content
|
|
end
|
|
end
|
|
|
|
def seed_params
|
|
params.require(:seed).permit(
|
|
:crop_id, :description, :quantity, :plant_before, :plant_before_year,
|
|
:parent_planting_id, :saved_at,
|
|
:days_until_maturity_min, :days_until_maturity_max,
|
|
:variety, :batch_number, :row_spacing, :how_to_sow, :germination_days_min, :germination_days_max,
|
|
:organic, :gmo, :source,
|
|
:heirloom, :tradable_to, :slug,
|
|
:finished, :finished_at, sow_months: []
|
|
)
|
|
end
|
|
|
|
def csv_filename
|
|
if @owner
|
|
"Growstuff-#{@owner.to_param}-Seeds-#{Time.zone.now.to_fs(:number)}.csv"
|
|
else
|
|
"Growstuff-Seeds-#{Time.zone.now.to_fs(:number)}.csv"
|
|
end
|
|
end
|
|
end
|