From 9d8f2f512fd47b848579d2aa05abff3eec19a933 Mon Sep 17 00:00:00 2001 From: Mae Beale Date: Mon, 12 Oct 2026 20:53:09 -0400 Subject: [PATCH 1/2] Start: link Leader spotlight FileMaker codes to the quotations archive Co-Authored-By: Claude Opus 5.5 (1M context) From 0fcec336a3332322bc91298c7894057a3b8eeec5 Mon Sep 17 00:00:00 2001 From: Mae Beale Date: Mon, 12 Oct 2026 20:55:41 -0400 Subject: [PATCH 2/2] Link a resource's FileMaker code to its quotation in the archive Production resource codes are quotation ids on Leader spotlights, but the code had no field on the resource form, so admins couldn't see it or fix the bad ones. The admin box now shows it with archive links, and a quotation's archive page links back. Co-Authored-By: Claude Opus 5.5 (1M context) --- AGENTS.md | 2 +- app/controllers/fm_archives_controller.rb | 2 +- app/helpers/filemaker_helper.rb | 1 + app/models/resource.rb | 2 ++ app/views/resources/_form.html.erb | 5 +++++ config/features.yml | 12 ++++++++++++ db/seeds/dev/fm_archive.rb | 2 ++ docs/adr/0005-filemaker-code-archive-mapping.md | 8 ++++---- spec/models/concerns/filemaker_coded_spec.rb | 1 + spec/requests/fm_archives_spec.rb | 9 +++++++++ spec/requests/resources_spec.rb | 10 ++++++++++ spec/seeds/fm_archive_seed_spec.rb | 10 ++++++++++ 12 files changed, 58 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 066e78c386..df71366b01 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -146,7 +146,7 @@ This codebase (Rails 8.1) | `Communicable` | Gives a record the `notifications` association, nested attributes, a default `communications_scope`, and a `before_validation` addressing a hand-logged communication to the record's `communications_email`. Included by every model whose form renders `shared/_comments_and_communications` | | `AuthorCreditable` | Author attribution. Credits are formatted by the credited **person's profile** (`Person#display_name_preference`), not by the record. The record's `author_credit_preference` is the consent snapshot taken at create time and is human-editable only on the author credit divergences page. Anonymity is a one-way latch: the profile or the record can set it, neither can strip it from the other. `credits_to_org` flips the unattributed label to "AWBW Staff"; `credits_creator` (idea models, workshop logs) credits the submitting account's person when no author is named | | `Featureable` | `featured`, `publicly_featured` scopes | -| `FilemakerCoded` | Reads `filemaker_code` as the trimmed, comma-separated **list** it is (Person, Organization): `#filemaker_codes` for the set a record answers to, `.join_filemaker_codes` to combine any mix of strings/lists into one normalized column value (both dedupers union the codes on merge). `filemaker_archived_in` names the `Fm*` archive table(s) a model's codes point into (`.filemaker_archives`), and `.with_filemaker_code(code)` finds records carrying a code. Nothing should compare the whole column against a single code — `FilemakerHelper#filemaker_code_links` renders one archive link per code, and `fm_archives#show` links back to the portal records carrying its code | +| `FilemakerCoded` | Reads `filemaker_code` as the trimmed, comma-separated **list** it is (Person, Organization, Resource): `#filemaker_codes` for the set a record answers to, `.join_filemaker_codes` to combine any mix of strings/lists into one normalized column value (both dedupers union the codes on merge). `filemaker_archived_in` names the `Fm*` archive table(s) a model's codes point into (`.filemaker_archives`), and `.with_filemaker_code(code)` finds records carrying a code. Nothing should compare the whole column against a single code — `FilemakerHelper#filemaker_code_links` renders one archive link per code, and `fm_archives#show` links back to the portal records carrying its code | | `Mentioner` | ActionText @mention extraction and grouping | | `NameFilterable` | Name-based filtering | | `PayChargeExtensions` | Included into `Pay::Charge` (via `config/initializers/pay.rb`): turns a Stripe charge into an AWBW `Payment` + `Allocation`, resolving the membership invoice it pays | diff --git a/app/controllers/fm_archives_controller.rb b/app/controllers/fm_archives_controller.rb index 77b4e7aa18..45e1ce237a 100644 --- a/app/controllers/fm_archives_controller.rb +++ b/app/controllers/fm_archives_controller.rb @@ -63,7 +63,7 @@ class FmArchivesController < ApplicationController { label: "Other", icon: "📎", tables: %w[notes] } ] - PORTAL_MODELS = [ Person, Organization ].freeze + PORTAL_MODELS = [ Person, Organization, Resource ].freeze def index authorize! :fm_archive diff --git a/app/helpers/filemaker_helper.rb b/app/helpers/filemaker_helper.rb index dbcdea1fea..dbb1f8f0c6 100644 --- a/app/helpers/filemaker_helper.rb +++ b/app/helpers/filemaker_helper.rb @@ -33,6 +33,7 @@ def filemaker_portal_link(record) text, path = case record when Person then [ record.full_name, person_path(record) ] when Organization then [ record.name, organization_path(record) ] + when Resource then [ record.title, resource_path(record) ] end link_to text, path, class: button_classes(:primary_outline, size: :sm) diff --git a/app/models/resource.rb b/app/models/resource.rb index 4c596c151a..8730aeb5c8 100644 --- a/app/models/resource.rb +++ b/app/models/resource.rb @@ -5,6 +5,8 @@ class Resource < ApplicationRecord include Rails.application.routes.url_helpers include ActionText::Attachable include Mentionable + include FilemakerCoded + filemaker_archived_in FmQuotation # Define rich text fields for mentions functionality def self.mentionable_rich_text_fields diff --git a/app/views/resources/_form.html.erb b/app/views/resources/_form.html.erb index 0a394342ee..d064006a3c 100644 --- a/app/views/resources/_form.html.erb +++ b/app/views/resources/_form.html.erb @@ -51,6 +51,11 @@ <%= visibility_flag_input f, :publicly_featured %> <%= visibility_flag_input f, :hidden_from_search %> + <%= f.input :filemaker_code, + label: "FileMaker quotation ID", + hint: filemaker_code_hint(f.object), + wrapper_html: { class: "mt-4" }, + input_html: { class: "w-full rounded border-gray-300 bg-blue-100" } %> <% end %> diff --git a/config/features.yml b/config/features.yml index 86b08a0409..d4da35e6e2 100644 --- a/config/features.yml +++ b/config/features.yml @@ -4236,3 +4236,15 @@ - "Separate several codes with commas. Spaces, tabs, and line breaks are ignored, and the list saves sorted." - "An organization's edit page also links the FileMaker organization its project belongs to." - "An organization's code can be a FileMaker project ID (like P2775) or organization ID (like ORG07206). Type the whole code, letters included." + +- name: "Open a Leader spotlight's original FileMaker quote" + area: stories + display_status: admin_facing + released_on: 2026-10-12 + pr_number: 2663 + summary: >- + A resource's edit page now shows its FileMaker quotation ID and links it to the + quote in the FileMaker archive, and each archived quote links back to the + resource that carries it. + pro_tips: + - "The ID sits in the admin box beside the visibility flags. A struck-through ID means the archive has no such quote — check it for a typo." diff --git a/db/seeds/dev/fm_archive.rb b/db/seeds/dev/fm_archive.rb index ac7ac822c4..e5660e7e63 100644 --- a/db/seeds/dev/fm_archive.rb +++ b/db/seeds/dev/fm_archive.rb @@ -68,6 +68,7 @@ def seed_fm(model, fm_ids) payments = (Payment.where(person: people).or(Payment.where(organization: organizations)).order(:id).first(3) + Payment.order(:id).limit(3)).uniq.first(3) workshops = Workshop.order(:title).first(3) +resources = (Resource.where(kind: "LeaderSpotlight").order(:title).first(3) + Resource.order(:title).first(3)).uniq.first(3) workshop_titles = [ workshops[0]&.title || "Comfort Journals", workshops[1]&.title || "Watercolor Windows", "Story Dolls" ] payment_amounts = [ payments[0], payments[1] ].map { |payment| payment && format("%.2f", payment.amount_cents / 100.0) } @@ -729,3 +730,4 @@ def seed_fm(model, fm_ids) link_codes.call(affiliations, fm_ids[:personnel]) link_codes.call(payments, fm_ids[:payment]) link_codes.call(workshops, fm_ids[:workshop]) +link_codes.call(resources, fm_ids[:quotation]) diff --git a/docs/adr/0005-filemaker-code-archive-mapping.md b/docs/adr/0005-filemaker-code-archive-mapping.md index 55643d059c..f8a39815ab 100644 --- a/docs/adr/0005-filemaker-code-archive-mapping.md +++ b/docs/adr/0005-filemaker-code-archive-mapping.md @@ -28,7 +28,7 @@ drive the archive pages' links. | `affiliations.filemaker_code` | `fm_projects`, then `fm_personnels` | `ProjectID`, e.g. `P2775`; rarely `PrsnlRecID`, e.g. `PS9999` | no UI link yet | | `payments.filemaker_code` | `fm_payments` | `RecordID`, e.g. `D29186` | dev seed (no UI link yet) | | `workshops.filemaker_code` | `fm_workshops` | `WorkshopID` | dev seed (no UI link yet) | -| `resources.filemaker_code` | `fm_quotations` | `RecordID`, e.g. `Q10215` | no UI link yet | +| `resources.filemaker_code` | `fm_quotations` | `RecordID`, e.g. `Q10215` | `filemaker_code_hint` on the resource form (admin box) | - **Organization → Project, or → Organization.** An app `Organization` is usually one art program site, which is a FileMaker *Project*. FileMaker's own Organization row is @@ -50,8 +50,8 @@ drive the archive pages' links. - **Resource → Quotation.** In production, 116 of 128 resource codes are quotation ids (`Q…`), 115 of them on Leader spotlight resources and one on a Story; 115 of the 116 exist in `fm_quotations` (quotes and testimonials, with a `WorkShopLeader` field). - Form Letters are mail-merge templates, not resources. The dev seed still leaves - `resources.filemaker_code` blank. + Form Letters are mail-merge templates, not resources. The dev seed gives Leader + spotlights quotation codes first. ## Archive id patterns @@ -173,6 +173,6 @@ Production, 2026-10-12: 84 records (41 organizations, 16 affiliations, 14 worksh ## Consequences -A UI link for affiliations, payments, workshops, or resources includes `FilemakerCoded` on the +A UI link for affiliations, payments, or workshops includes `FilemakerCoded` on the model and declares the archive table above (e.g. `filemaker_archived_in FmPersonnel`); `filemaker_code_links` reads it from there. diff --git a/spec/models/concerns/filemaker_coded_spec.rb b/spec/models/concerns/filemaker_coded_spec.rb index 6098393b91..14ecc677aa 100644 --- a/spec/models/concerns/filemaker_coded_spec.rb +++ b/spec/models/concerns/filemaker_coded_spec.rb @@ -65,6 +65,7 @@ it "names the archive tables each coded model points into" do expect(Person.filemaker_archives).to eq([ FmRolodex ]) expect(Organization.filemaker_archives).to eq([ FmProject, FmOrganization ]) + expect(Resource.filemaker_archives).to eq([ FmQuotation ]) end end end diff --git a/spec/requests/fm_archives_spec.rb b/spec/requests/fm_archives_spec.rb index e4e16165e5..6e413cccb3 100644 --- a/spec/requests/fm_archives_spec.rb +++ b/spec/requests/fm_archives_spec.rb @@ -65,6 +65,15 @@ expect(response.body.scan(%r{href="/organizations/\d+"}).size).to eq(2) end + it "links an archived quotation back to the resource carrying its code" do + FmQuotation.create!(fm_id: "Q10215") + resource = create(:resource, kind: "LeaderSpotlight", title: "Spotlight on a leader", filemaker_code: "Q10215") + + get fm_archive_path("Q10215", table: "quotations") + + expect(response.body).to include(resource_path(resource), "Spotlight on a leader") + end + it "omits the portal section when nothing carries the code" do FmRolodex.create!(fm_id: "00006") diff --git a/spec/requests/resources_spec.rb b/spec/requests/resources_spec.rb index 507198ab3f..445a933d1b 100644 --- a/spec/requests/resources_spec.rb +++ b/spec/requests/resources_spec.rb @@ -133,6 +133,16 @@ expect(response.body).to include('name="resource[publicly_visible]"') expect(response.body).to include(VisibilityFlagsHelper::FLAG_DEFINITIONS[:hidden_from_search][:description]) end + + it "links the FileMaker code to its quotation in the archive" do + FmQuotation.create!(fm_id: "Q10215") + resource = Resource.create! valid_attributes.merge(filemaker_code: "Q10215, 21") + get edit_resource_url(resource) + + expect(response.body).to include('name="resource[filemaker_code]"') + expect(response.body).to include(fm_archive_path("Q10215", table: "quotations")) + expect(response.body).to include(%(title="No FileMaker archive record for 21")) + end end describe "POST /create" do diff --git a/spec/seeds/fm_archive_seed_spec.rb b/spec/seeds/fm_archive_seed_spec.rb index ca0f67fede..65e2aa087e 100644 --- a/spec/seeds/fm_archive_seed_spec.rb +++ b/spec/seeds/fm_archive_seed_spec.rb @@ -34,6 +34,16 @@ expect(FmRolodex.find_by!(fm_id: "00901").data.values_at("FirstName", "LastName")).to eq(%w[Abe Archer]) end + it "gives Leader spotlights quotation codes ahead of other resources" do + other = create(:resource, kind: "Toolkit", title: "Aaa toolkit") + spotlight = create(:resource, kind: "LeaderSpotlight", title: "Zed spotlight") + + load seed + + expect(spotlight.reload.filemaker_code).to eq("Q90001") + expect(other.reload.filemaker_code).to eq("Q90002, Q90008") + end + it "is idempotent" do load seed expect { load seed }.not_to change(FmPayment, :count)