From cd6f8f1876a5a6b11bcfe8a24eeaaa491db2e289 Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Wed, 28 Oct 2026 20:44:37 +1300 Subject: [PATCH] feat: add packet labeling functionality and awaiting text scope for seed photos --- app/helpers/photos_helper.rb | 6 +++ app/models/photo_association.rb | 3 ++ app/models/seed.rb | 6 +++ app/views/photos/_packet_label.html.haml | 12 +++--- lib/tasks/packets.rake | 5 +-- script/ocr/read_packets.py | 2 +- .../photo_association_awaiting_text_spec.rb | 37 +++++++++++++++++++ spec/models/seed_packet_photo_spec.rb | 13 +++++++ 8 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 spec/models/photo_association_awaiting_text_spec.rb diff --git a/app/helpers/photos_helper.rb b/app/helpers/photos_helper.rb index 9b24f1716..d1d8c3e02 100644 --- a/app/helpers/photos_helper.rb +++ b/app/helpers/photos_helper.rb @@ -18,6 +18,12 @@ module PhotosHelper { photo_dialog: true, photo_label: photo_label(model) } end + # The link that sets which side of a seed's packet a photo of it is of; an + # empty label says it is not of the packet. + def packet_label_path(seed, photo, label) + photo_association_path(photo_id: photo.id, type: 'seed', id: seed.id, label:) + end + def crop_image_path(crop) thumbnail_url(crop.default_photo) end diff --git a/app/models/photo_association.rb b/app/models/photo_association.rb index 82f7a9150..6d5c42761 100644 --- a/app/models/photo_association.rb +++ b/app/models/photo_association.rb @@ -27,6 +27,9 @@ class PhotoAssociation < ApplicationRecord # Photos of a seed packet say what the packet says, not what the crop looks # like, so a photo labelled as one is left out of a crop's own photos. scope :of_packets, -> { where(photographable_type: 'Seed').where.not(label: nil) } + # Those of a seed that has no text from its packet yet, which is what still + # needs reading. + scope :awaiting_text, -> { of_packets.where(photographable_id: Seed.unscoped.where(packet_text: [nil, '']).select(:id)) } def self.item(item_id, item_type) find_by!(photographable_id: item_id, photographable_type: item_type).photographable diff --git a/app/models/seed.rb b/app/models/seed.rb index f940556e6..b102621f0 100644 --- a/app/models/seed.rb +++ b/app/models/seed.rb @@ -60,6 +60,12 @@ class Seed < ApplicationRecord photo_associations.find_by(label: side)&.photo end + # Which side of the packet a photo of this seed is of, if it is of one. Uses + # the loaded photo associations, so a page of photos reads them once. + def label_of(photo) + photo_associations.find { |association| association.photo_id == photo.id }&.label + end + # The packet's front, if there is a photo of it. Otherwise the most liked # photo that is not of the back. def default_photo diff --git a/app/views/photos/_packet_label.html.haml b/app/views/photos/_packet_label.html.haml index 8f951296b..75b8c1112 100644 --- a/app/views/photos/_packet_label.html.haml +++ b/app/views/photos/_packet_label.html.haml @@ -1,13 +1,11 @@ -# On a seed's own page: whether each of its photos is of the packet, and which -# side. Everyone sees what it is; the seed's owner can change it. -- label = seed.photo_associations.find { |association| association.photo_id == photo.id }&.label -- label_path = ->(choice) { photo_association_path(photo_id: photo.id, type: 'seed', id: seed.id, label: choice) } +- label = seed.label_of(photo) .packet-label.small.mt-1 - if label %span.badge.text-bg-secondary= t("seeds.packet.label_#{label}") - if can?(:update, seed) - - choices = PhotoAssociation::LABELS['Seed'].without(label) - - choices.each do |choice| - = link_to t("seeds.packet.mark_#{choice}"), label_path.call(choice), method: :patch, class: 'me-2' - - if label - = link_to t('seeds.packet.not_packet'), label_path.call(''), method: :patch, class: 'me-2' + - PhotoAssociation::LABELS['Seed'].without(label).each do |choice| + = link_to t("seeds.packet.mark_#{choice}"), packet_label_path(seed, photo, choice), method: :patch, class: 'me-2' + - not_packet_path = packet_label_path(seed, photo, '') + = link_to t('seeds.packet.not_packet'), not_packet_path, method: :patch, class: 'me-2' if label diff --git a/lib/tasks/packets.rake b/lib/tasks/packets.rake index ed27f25f7..800efa111 100644 --- a/lib/tasks/packets.rake +++ b/lib/tasks/packets.rake @@ -5,10 +5,7 @@ namespace :packets do # usage: rake packets:needing_ocr LIMIT=5 task needing_ocr: :environment do limit = ENV.fetch('LIMIT', nil).to_i - associations = PhotoAssociation.of_packets - .joins('INNER JOIN seeds ON seeds.id = photo_associations.photographable_id') - .where(seeds: { packet_text: [nil, ''] }) - .includes(:photo).order(:photo_id, :id) + associations = PhotoAssociation.awaiting_text.includes(:photo).order(:photo_id, :id) associations = associations.limit(limit) if limit.positive? associations.each do |association| diff --git a/script/ocr/read_packets.py b/script/ocr/read_packets.py index 5eadcc4e1..f3171a96f 100755 --- a/script/ocr/read_packets.py +++ b/script/ocr/read_packets.py @@ -31,12 +31,12 @@ Needs only the python3 standard library, and whatever OCR command is used. import argparse import json +import os import re import shlex import subprocess import sys import tempfile -import os import urllib.error import urllib.request diff --git a/spec/models/photo_association_awaiting_text_spec.rb b/spec/models/photo_association_awaiting_text_spec.rb new file mode 100644 index 000000000..2c25d6bd0 --- /dev/null +++ b/spec/models/photo_association_awaiting_text_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe PhotoAssociation, '.awaiting_text' do + let(:seed) { create(:seed) } + + def attach(item, label) + item.photo_associations.create!(photo: create(:photo, owner: item.owner), label:) + end + + it 'has the photos of a packet whose seed has no text' do + association = attach(seed, 'back') + expect(described_class.awaiting_text).to eq [association] + end + + it 'has the photos of a packet whose seed has only empty text' do + seed.update!(packet_text: '') + association = attach(seed, 'front') + expect(described_class.awaiting_text).to eq [association] + end + + it 'leaves out a seed that has its text' do + attach(create(:seed, packet_text: 'Sow by: December 2026'), 'back') + expect(described_class.awaiting_text).to be_empty + end + + it 'leaves out a photo that is not of the packet' do + attach(seed, nil) + expect(described_class.awaiting_text).to be_empty + end + + it 'leaves out the photos of something that is not a seed' do + attach(create(:job), 'before') + expect(described_class.awaiting_text).to be_empty + end +end diff --git a/spec/models/seed_packet_photo_spec.rb b/spec/models/seed_packet_photo_spec.rb index 0888c1b47..2eb1871bf 100644 --- a/spec/models/seed_packet_photo_spec.rb +++ b/spec/models/seed_packet_photo_spec.rb @@ -21,4 +21,17 @@ describe Seed, '#packet_photo' do it('is nothing') { expect(seed.packet_photo('back')).to be_nil } end + + describe '#label_of' do + it('is front for the front') { expect(seed.reload.label_of(front)).to eq 'front' } + it('is back for the back') { expect(seed.reload.label_of(back)).to eq 'back' } + + it('is nothing for a photo that is not of the packet') do + expect(seed.reload.label_of(seed.photos.reject { |photo| [front, back].include?(photo) }.first)).to be_nil + end + + it('is nothing for a photo that is not of this seed') do + expect(seed.reload.label_of(create(:photo, owner: seed.owner))).to be_nil + end + end end