From 42deef49f6ba3d9e13c77e3e0bf02714c99be0be Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Fri, 23 Oct 2026 08:31:29 +1300 Subject: [PATCH] Enhance job handling in neighbours and timeline services: add support for finished jobs, update queries, and improve localization for job-related events. --- app/helpers/neighbours_helper.rb | 13 +++++++++---- app/services/neighbours.rb | 4 ++-- app/services/timeline_service.rb | 13 ++++++++++--- config/locales/en.yml | 3 +++ spec/features/neighbours_spec.rb | 9 +++++++++ spec/services/neighbours_spec.rb | 22 ++++++++++++++++++++++ 6 files changed, 55 insertions(+), 9 deletions(-) diff --git a/app/helpers/neighbours_helper.rb b/app/helpers/neighbours_helper.rb index e7ef5c579..7ae57d761 100644 --- a/app/helpers/neighbours_helper.rb +++ b/app/helpers/neighbours_helper.rb @@ -28,7 +28,12 @@ module NeighboursHelper day == today - 1 ? t('neighbours.day.yesterday', date:) : date end - GROUP_ORDER = %w(planting seed seed_trade harvest transplant planted_out planting_finished seed_finished job garden_layout).freeze + # Planned and finished jobs, which have a name of their own and no page. + JOB_TYPES = %w(job job_finished).freeze + NAMED_TYPES = (JOB_TYPES + %w(garden_layout)).freeze + + GROUP_ORDER = %w(planting seed seed_trade harvest transplant planted_out planting_finished seed_finished + job_finished job garden_layout).freeze # The icon that goes with each kind of thing someone did. def group_icon(kind) @@ -37,7 +42,7 @@ module NeighboursHelper when 'seed', 'seed_trade' then seed_icon when 'harvest' then harvest_icon when 'transplant', 'planted_out' then growing_icon - when 'planting_finished', 'seed_finished' then finished_icon + when 'planting_finished', 'seed_finished', 'job_finished' then finished_icon when 'job' then icon('fas', 'calendar-days') when 'garden_layout' then garden_icon when 'photo' then photo_icon @@ -156,7 +161,7 @@ module NeighboursHelper # Jobs have no page of their own, so they link to their owner's list. def thing_path(item) - item.type == 'job' ? member_jobs_path(item.record.owner) : polymorphic_path(thing_record(item)) + JOB_TYPES.include?(item.type) ? member_jobs_path(item.record.owner) : polymorphic_path(thing_record(item)) end def thing_record(item) @@ -165,7 +170,7 @@ module NeighboursHelper def thing_name(item) record = thing_record(item) - %w(job garden_layout).include?(item.type) ? record.name : record.crop.name + NAMED_TYPES.include?(item.type) ? record.name : record.crop.name end def thing_details(item) diff --git a/app/services/neighbours.rb b/app/services/neighbours.rb index fed44d508..fc71afa12 100644 --- a/app/services/neighbours.rb +++ b/app/services/neighbours.rb @@ -17,12 +17,12 @@ class Neighbours NEARBY_LIMIT = 100 SUGGESTIONS_SHOWN = 4 POSTS_SHOWN = 5 - FEED_KINDS = %i(planting harvest seed photo garden_layout transplant planned_job planting_finished seed_finished).freeze + FEED_KINDS = %i(planting harvest seed photo garden_layout transplant planned_job job_finished planting_finished seed_finished).freeze FEED_EVENTS = 15 MAX_OFFSET = 500 PHOTO_SUBJECTS = %w(Planting Harvest Seed Garden).freeze EVENT_MODELS = { 'planting' => Planting, 'harvest' => Harvest, 'seed' => Seed, 'photo' => Photo, 'garden_layout' => Garden, - 'transplant' => PlantingHistory, 'job' => Job, 'planting_finished' => Planting, + 'transplant' => PlantingHistory, 'job' => Job, 'job_finished' => Job, 'planting_finished' => Planting, 'seed_finished' => Seed }.freeze FEED_CACHE_FOR = 5.minutes diff --git a/app/services/timeline_service.rb b/app/services/timeline_service.rb index ca37af3a8..a5176e805 100644 --- a/app/services/timeline_service.rb +++ b/app/services/timeline_service.rb @@ -18,8 +18,9 @@ class TimelineService comment: :comments_query, photo: :photos_query, seed: :seeds_query, - job: :jobs_query, - planned_job: :planned_jobs_query, + job: :jobs_query, + planned_job: :planned_jobs_query, + job_finished: :jobs_finished_query, like: :likes_query, garden_layout: :garden_layouts_query, transplant: :transplants_query, @@ -30,7 +31,7 @@ class TimelineService # Events of the given kinds, newest first. Garden layouts are only here when # asked for, since the timeline page has nothing to say about them; the same goes - # for transplants, planned jobs and finished plantings and seeds. + # for transplants, planned and finished jobs, and finished plantings and seeds. def self.query(kinds = DEFAULT_EVENTS) kinds.map { |kind| public_send(EVENT_QUERIES.fetch(kind)) } .reduce { |all, one| all.union_all(one) } @@ -68,6 +69,12 @@ class TimelineService "null as crop_id") end + # Jobs as the day they were crossed off. + def self.jobs_finished_query + Job.where(finished: true).where.not(finished_at: nil) + .select(:id, "'job_finished' as event_type", 'jobs.finished_at as event_at', :owner_id, 'null as crop_id') + end + # Jobs as the day they were planned rather than the day they are due, # for a feed of what people have done. def self.planned_jobs_query diff --git a/config/locales/en.yml b/config/locales/en.yml index c6c87f1a7..a67081275 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -445,6 +445,9 @@ en: empty: Nothing to show yet. title: Latest from your friends and neighbours group: + job_finished_html: + one: Finished a job, %{things} + other: Finished %{count} jobs, %{things} job_html: one: Planned a job, %{things} other: Planned %{count} jobs, %{things} diff --git a/spec/features/neighbours_spec.rb b/spec/features/neighbours_spec.rb index 6e354e840..498d927c9 100644 --- a/spec/features/neighbours_spec.rb +++ b/spec/features/neighbours_spec.rb @@ -123,6 +123,15 @@ describe 'My friends and neighbours' do expect(page).to have_css('article', text: /friendly.*Planned a job, Weed the beans \(for #{due}\)/m) end + it 'shows a friend finishing a job' do + friend = create(:member, login_name: 'friendly') + member.follows.create!(followed: friend) + create(:job, owner: friend, name: 'Net the berries', finished: true) + + visit neighbours_path + expect(page).to have_css('article', text: /friendly.*Finished a job, Net the berries/m) + end + it 'shows only their own job under Me' do friend = create(:member, login_name: 'friendly') member.follows.create!(followed: friend) diff --git a/spec/services/neighbours_spec.rb b/spec/services/neighbours_spec.rb index a32c6e681..b4df88e56 100644 --- a/spec/services/neighbours_spec.rb +++ b/spec/services/neighbours_spec.rb @@ -229,6 +229,28 @@ describe Neighbours do expect(items.map { |item| [item.type, item.record] }).to include(['job', job]) end + it 'has jobs people finished' do + job = create(:job, owner: friend, finished: true) + + items = neighbours.feed.flat_map(&:updates).flat_map(&:items) + expect(items.map { |item| [item.type, item.record] }).to include(['job_finished', job]) + end + + it 'leaves out a job that is not finished as a finished job' do + create(:job, owner: friend, finished: false) + + items = neighbours.feed.flat_map(&:updates).flat_map(&:items) + expect(items.map(&:type)).not_to include('job_finished') + end + + it 'dates a finished job by when it was crossed off, not when it was due' do + job = create(:job, owner: friend, due_date: Time.zone.today - 1.month, finished: true) + job.update_column(:finished_at, Time.zone.now) # rubocop:disable Rails/SkipsModelValidations + + days = neighbours.feed.select { |day| day.updates.flat_map(&:items).any? { |item| item.type == 'job_finished' } } + expect(days.first.day).to eq Time.zone.today + end + it 'dates a job by when it was planned, not when it is due' do create(:job, owner: friend, due_date: Time.zone.today + 1.month)