From 64e98373332e21afd9f5f26e143b5c19ef088016 Mon Sep 17 00:00:00 2001 From: Mae Beale Date: Mon, 12 Oct 2026 21:27:35 -0400 Subject: [PATCH] Link an affiliation's FileMaker code to the archive, and back The affiliation edit page took a FileMaker code but offered no way to open it. Per ADR-0005, production affiliation codes are their program's project id, with one real Personnel id, so affiliations look a code up in projects, then personnel. A project's archive page lists the affiliations carrying its code alongside its org. 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/affiliation.rb | 3 +++ app/views/affiliations/edit.html.erb | 1 + config/features.yml | 11 +++++++++++ .../0005-filemaker-code-archive-mapping.md | 4 ++-- spec/helpers/filemaker_helper_spec.rb | 16 ++++++++++++++++ spec/models/concerns/filemaker_coded_spec.rb | 1 + spec/requests/fm_archives_spec.rb | 19 +++++++++++++++++++ 10 files changed, 56 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 066e78c386..db37359ab7 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, Affiliation): `#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..48891710c9 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, Affiliation ].freeze def index authorize! :fm_archive diff --git a/app/helpers/filemaker_helper.rb b/app/helpers/filemaker_helper.rb index dbcdea1fea..c646c1f070 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 Affiliation then [ "#{record.person&.full_name} at #{record.organization&.name}", edit_affiliation_path(record) ] end link_to text, path, class: button_classes(:primary_outline, size: :sm) diff --git a/app/models/affiliation.rb b/app/models/affiliation.rb index fffee4aa27..0a9a4a1168 100644 --- a/app/models/affiliation.rb +++ b/app/models/affiliation.rb @@ -1,5 +1,8 @@ class Affiliation < ApplicationRecord include Communicable + include FilemakerCoded + filemaker_archived_in FmProject, FmPersonnel + # Standing title given to the "facilitator affiliation" we create on registration # and org linking. Matches the `facilitators` scope / `facilitator?` predicate # (both treat exactly "Facilitator" as canonical). diff --git a/app/views/affiliations/edit.html.erb b/app/views/affiliations/edit.html.erb index 7f59588960..670a14f856 100644 --- a/app/views/affiliations/edit.html.erb +++ b/app/views/affiliations/edit.html.erb @@ -77,6 +77,7 @@
<%= f.input :filemaker_code, label: "FileMaker code", + hint: filemaker_code_hint(@affiliation), label_html: { class: "block text-xs font-medium text-gray-700 mb-1" }, input_html: { class: "w-full rounded-md border-gray-300 bg-white shadow-sm focus:border-blue-500 focus:ring-blue-200 text-sm" } %>
diff --git a/config/features.yml b/config/features.yml index 6400d70d81..f1cf79f513 100644 --- a/config/features.yml +++ b/config/features.yml @@ -4250,3 +4250,14 @@ - "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 an affiliation's FileMaker record" + area: people + display_status: admin_facing + released_on: 2026-10-12 + pr_number: 2649 + summary: >- + An affiliation's edit page now links its FileMaker code to the archive, and + the archive record links back to the affiliation. + pro_tips: + - "An affiliation's code is usually its program's FileMaker project ID (like P2775), and occasionally a personnel ID (like PS9999)." diff --git a/docs/adr/0005-filemaker-code-archive-mapping.md b/docs/adr/0005-filemaker-code-archive-mapping.md index 55643d059c..76e70c197e 100644 --- a/docs/adr/0005-filemaker-code-archive-mapping.md +++ b/docs/adr/0005-filemaker-code-archive-mapping.md @@ -25,7 +25,7 @@ drive the archive pages' links. |---|---|---|---| | `people.filemaker_code` | `fm_rolodexes` | `ID`, e.g. `00006` | `filemaker_code_links` on person edit + admin details | | `organizations.filemaker_code` | `fm_projects`, then `fm_organizations` | `ProjectID`, e.g. `P2775`; `OrgID`, e.g. `ORG07206` | `filemaker_code_links` on org form; `filemaker_project_links` / `filemaker_organization_links` on org edit | -| `affiliations.filemaker_code` | `fm_projects`, then `fm_personnels` | `ProjectID`, e.g. `P2775`; rarely `PrsnlRecID`, e.g. `PS9999` | no UI link yet | +| `affiliations.filemaker_code` | `fm_projects`, then `fm_personnels` | `ProjectID`, e.g. `P2775`; rarely `PrsnlRecID`, e.g. `PS9999` | `filemaker_code_links` on affiliation edit | | `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 | @@ -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 payments, workshops, or resources 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/helpers/filemaker_helper_spec.rb b/spec/helpers/filemaker_helper_spec.rb index 4485c1a52b..777d2cf852 100644 --- a/spec/helpers/filemaker_helper_spec.rb +++ b/spec/helpers/filemaker_helper_spec.rb @@ -66,6 +66,22 @@ expect(helper.filemaker_code_links(person)).to be_nil end end + + context "with a record archived in more than one table" do + let(:affiliation) { build(:affiliation, filemaker_code: "P2775, PS9999") } + + before do + FmProject.create!(fm_id: "P2775") + FmPersonnel.create!(fm_id: "PS9999") + end + + it "links each code to whichever table holds it" do + html = helper.filemaker_code_links(affiliation) + + expect(html).to include(fm_archive_path("P2775", table: "projects")) + expect(html).to include(fm_archive_path("PS9999", table: "personnels")) + end + end end describe "#filemaker_code_hint" do diff --git a/spec/models/concerns/filemaker_coded_spec.rb b/spec/models/concerns/filemaker_coded_spec.rb index 6098393b91..4c1d7bd465 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(Affiliation.filemaker_archives).to eq([ FmProject, FmPersonnel ]) end end end diff --git a/spec/requests/fm_archives_spec.rb b/spec/requests/fm_archives_spec.rb index e4e16165e5..128c65b9fd 100644 --- a/spec/requests/fm_archives_spec.rb +++ b/spec/requests/fm_archives_spec.rb @@ -65,6 +65,25 @@ expect(response.body.scan(%r{href="/organizations/\d+"}).size).to eq(2) end + it "links an archived project back to the affiliations carrying its code" do + FmProject.create!(fm_id: "P2775") + affiliation = create(:affiliation, filemaker_code: "P2775") + + get fm_archive_path("P2775", table: "projects") + + expect(response.body).to include(edit_affiliation_path(affiliation)) + end + + it "links an archived personnel row back to its portal affiliation" do + FmPersonnel.create!(fm_id: "PS9999") + affiliation = create(:affiliation, filemaker_code: "PS9999") + + get fm_archive_path("PS9999", table: "personnels") + + expect(response.body).to include(edit_affiliation_path(affiliation)) + expect(response.body).to include(ERB::Util.html_escape("#{affiliation.person.full_name} at #{affiliation.organization.name}")) + end + it "omits the portal section when nothing carries the code" do FmRolodex.create!(fm_id: "00006")