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)