Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4679830
Start: FileMaker archive links from people, orgs, and affiliations
maebeale Oct 12, 2026
eb0c69d
Put FileMaker archive buttons beside person and org codes, and link back
maebeale Oct 12, 2026
54ec7f0
Name a single-code archive button plainly and show missing codes stru…
maebeale Oct 12, 2026
6130db7
List FileMaker archive links in the code field's hint
maebeale Oct 12, 2026
9fc5b1d
Read FileMaker codes as comma-separated, ignoring any whitespace
maebeale Oct 12, 2026
47768c1
Call the org's parent FileMaker record an FM organization
maebeale Oct 12, 2026
24c6f3d
Show an edit page's FileMaker nav button only when it leads somewhere
maebeale Oct 12, 2026
4976121
Point ADR-0005 at filemaker_archived_in for new archive links
maebeale Oct 12, 2026
3cc41d5
Match an org's FileMaker code in full against projects or FM organiza…
maebeale Oct 12, 2026
1acc106
Record production FileMaker id patterns in ADR-0005
maebeale Oct 13, 2026
b06acac
Record what FileMaker codes portal records hold in production
maebeale Oct 13, 2026
80c64d4
Mark affiliations' P-codes as unverified in ADR-0005
maebeale Oct 13, 2026
1de431f
List an FM organization's portal orgs through its projects too
maebeale Oct 13, 2026
0c45237
Map resource FileMaker codes to quotations in ADR-0005
maebeale Oct 13, 2026
952d30e
Map affiliation FileMaker codes to projects in ADR-0005
maebeale Oct 13, 2026
1f2b9d1
Add hand-fix guidance for bad FileMaker codes to ADR-0005
maebeale Oct 13, 2026
88754c1
List the records with FileMaker codes to fix by hand in ADR-0005
maebeale Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). Nothing should compare the whole column against a single code β€” the `filemaker_code_links` helper renders one archive link per code |
| `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 |
| `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 |
Expand Down
16 changes: 16 additions & 0 deletions app/controllers/fm_archives_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ class FmArchivesController < ApplicationController
{ label: "Other", icon: "πŸ“Ž", tables: %w[notes] }
]

PORTAL_MODELS = [ Person, Organization ].freeze

def index
authorize! :fm_archive

Expand Down Expand Up @@ -92,10 +94,24 @@ def show
end

load_backlinks
load_portal_records
end

private

def load_portal_records
@portal_records = PORTAL_MODELS
.select { |model| model.filemaker_archives.include?(@record.class) }
.flat_map { |model| model.with_filemaker_code(@record.fm_id).to_a }
@portal_records |= organizations_via_projects if @record.is_a?(FmOrganization)
end

# Most orgs carry a project code, reaching their FM organization through Project.OrgID.
def organizations_via_projects
project_ids = FmProject.where("data->>'$.OrgID' = ?", @record.fm_id).pluck(:fm_id)
Organization.with_any_filemaker_code(project_ids).to_a
end

def load_backlinks
@backlinks = {}
return unless @record.class.const_defined?(:HAS_MANY)
Expand Down
64 changes: 54 additions & 10 deletions app/helpers/filemaker_helper.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,62 @@
module FilemakerHelper
# One archive link per FileMaker code a record carries, since a record can answer to
# several (see FilemakerCoded). A code the archive has no row for renders as plain
# text. `label` prefixes each link, for a button or hint that has to read as an action.
def filemaker_code_links(record, archive:, table:, link_class: nil, label: nil)
codes = record.filemaker_codes
return if codes.empty?
# several (see FilemakerCoded). A `label` names the button, adding each code only when
# there are several to tell apart.
def filemaker_code_links(record, link_class: nil, label: nil, linked_only: false)
filemaker_archive_links(record.filemaker_codes, archives: record.class.filemaker_archives,
link_class: link_class, label: label, linked_only: linked_only)
end

# The hint under a FileMaker code input, linking each code to its archive record.
def filemaker_code_hint(record)
links = filemaker_code_links(record, link_class: "text-blue-600 underline hover:text-blue-800")
safe_join([ "In the archive:", links ], " ") if links
end

# The FM projects among an org's codes.
def filemaker_project_links(organization, link_class: nil, label: nil, linked_only: false)
project_ids = FmProject.where(fm_id: organization.filemaker_codes).pluck(:fm_id)
filemaker_archive_links(project_ids, archives: [ FmProject ], link_class: link_class, label: label, linked_only: linked_only)
end

# The FM organizations among an org's codes, plus the one each of its projects sits under.
def filemaker_organization_links(organization, link_class: nil, label: nil, linked_only: false)
codes = organization.filemaker_codes
parent_ids = FmProject.where(fm_id: codes).filter_map { |project| project.data["OrgID"].presence }
fm_organization_ids = (FmOrganization.where(fm_id: codes).pluck(:fm_id) + parent_ids).uniq
filemaker_archive_links(fm_organization_ids, archives: [ FmOrganization ],
link_class: link_class, label: label, linked_only: linked_only)
end

# The way back from an archive row to the portal record carrying its code.
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) ]
end

link_to text, path, class: button_classes(:primary_outline, size: :sm)
end

# A code no archive table holds renders struck through, flagging a likely typo β€”
# or is dropped with `linked_only`, for nav buttons that must all lead somewhere.
def filemaker_archive_links(codes, archives:, link_class: nil, label: nil, linked_only: false)
return if codes.empty? || !allowed_to?(:show?, :fm_archive)

stored = archives.index_with { |archive| archive.where(fm_id: codes).pluck(:fm_id).to_set }
archive_by_code = codes.index_with { |code| archives.find { |candidate| stored[candidate].include?(code) } }
archive_by_code.compact! if linked_only
return if archive_by_code.empty?

stored = archive.where(fm_id: codes).pluck(:fm_id).to_set
show_code = label.nil? || archive_by_code.many?

safe_join(codes.map do |code|
text = [ label, code ].compact.join(" ")
next tag.span(text) unless stored.include?(code)
safe_join(archive_by_code.map do |code, archive|
text = show_code ? safe_join([ label, code ].compact, " ") : label
# line-through! outranks a link_class `underline`, which sets the same text-decoration-line.
next tag.span(text, class: [ link_class, "line-through! opacity-60" ], title: "No FileMaker archive record for #{code}") unless archive

link_to text, fm_archive_path(code, table: table), class: link_class, target: "_blank", rel: "noopener"
link_to text, fm_archive_path(code, table: archive.table_name.delete_prefix("fm_")),
class: link_class, title: "Open #{code} in the FileMaker archive", target: "_blank", rel: "noopener"
end, " ")
end
end
30 changes: 27 additions & 3 deletions app/models/concerns/filemaker_coded.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
# One record can answer to more than one FileMaker code β€” two records that each mapped
# to a FileMaker row get merged, or the archive itself holds several rows for one
# person. The `filemaker_code` column holds them as a trimmed, comma-separated list;
# person. The `filemaker_code` column holds them as a sorted, comma-separated list;
# this is the seam for reading and combining them, so nothing compares the whole
# column against a single code.
# column against a single code. Codes never hold whitespace, so any (tabs, line
# breaks, pasted non-breaking spaces) is dropped.
module FilemakerCoded
extend ActiveSupport::Concern

included do
class_attribute :filemaker_archives, instance_writer: false, default: []

normalizes :filemaker_code, with: ->(value) { join_filemaker_codes(value) }, apply_to_nil: true
end

def filemaker_codes
self.class.split_filemaker_codes(filemaker_code)
end

class_methods do
# The Fm* archive tables this model's codes point into, checked in order.
def filemaker_archived_in(*archives)
self.filemaker_archives = archives
end

def with_filemaker_code(code)
where("FIND_IN_SET(?, REGEXP_REPLACE(filemaker_code, '[[:space:]]', ''))", strip_filemaker_code(code))
end

def with_any_filemaker_code(codes)
codes.map { |code| with_filemaker_code(code) }.reduce(:or) || none
end

def split_filemaker_codes(value)
value.to_s.split(",").map(&:strip).reject(&:blank?).uniq
value.to_s.split(",").map { |code| strip_filemaker_code(code) }.reject(&:empty?).uniq
end

def strip_filemaker_code(code)
code.to_s.gsub(/[[:space:]]/, "")
end

# A single normalized column value from any mix of code strings/lists.
Expand Down
1 change: 1 addition & 0 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class Organization < ApplicationRecord
include RemoteSearchable, TagFilterable, Trendable, WindowsTypeFilterable, SectorsTaggable, AgeGroupTaggable # Publishable
include Communicable
include FilemakerCoded
filemaker_archived_in FmProject, FmOrganization
belongs_to :organization_status
belongs_to :created_by, class_name: "User", optional: true
belongs_to :updated_by, class_name: "User", optional: true
Expand Down
1 change: 1 addition & 0 deletions app/models/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class Person < ApplicationRecord
include RemoteSearchable, TagFilterable, Trendable, WindowsTypeFilterable, SectorsTaggable, AgeGroupTaggable, StaffTaggable
include Communicable
include FilemakerCoded
filemaker_archived_in FmRolodex

pay_customer default_payment_processor: :stripe

Expand Down
11 changes: 11 additions & 0 deletions app/views/fm_archives/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
<%= link_to "Back to list", fm_archives_path(table: @record.class.table_name.sub("fm_", "")), class: button_classes(:primary_outline) %>
</div>

<% if @portal_records.any? %>
<div class="rounded-xl bg-white p-6">
<h2 class="mb-3 text-lg font-semibold">In the portal</h2>
<div class="flex flex-wrap gap-2">
<% @portal_records.each do |record| %>
<%= filemaker_portal_link(record) %>
<% end %>
</div>
</div>
<% end %>

<div class="rounded-xl bg-white p-6">
<dl class="divide-y divide-gray-200">
<% @record.data.each do |key, value| %>
Expand Down
9 changes: 2 additions & 7 deletions app/views/organizations/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -179,16 +179,11 @@
<% if allowed_to?(:manage?, Organization) %>
<div class="admin-only bg-blue-100">
<%= f.input :filemaker_code,
label: "FileMaker Project ID",
label: "FileMaker project or organization ID",
hint: filemaker_code_hint(f.object),
input_html: {
class: "rounded-md border-gray-300 bg-blue-100 shadow-sm focus:ring-blue-500 focus:border-blue-500"
} %>
<%- if params[:admin] == "true" %>
<p class="mt-1 text-xs">
<%= filemaker_code_links(f.object, archive: FmProject, table: "projects", label: "View archive",
link_class: "text-blue-600 hover:text-blue-800 underline") %>
</p>
<% end %>
<% high_profile_gem = tag.i(class: "fa-solid fa-gem text-purple-600", aria: { hidden: true }) %>
<%= f.input :high_profile,
label: "High profile",
Expand Down
8 changes: 4 additions & 4 deletions app/views/organizations/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
<% end %>
<% end %>
<% actions = capture do %>
<% if params[:admin] == "true" %>
<%= filemaker_code_links(@organization, archive: FmProject, table: "projects", label: "FM project",
link_class: "admin-only bg-blue-100 text-sm #{eyebrow_link_class} px-2 py-1") %>
<% end %>
<%= filemaker_project_links(@organization, label: "FM project", linked_only: true,
link_class: "admin-only bg-blue-100 text-sm #{eyebrow_link_class} px-2 py-1") %>
<%= filemaker_organization_links(@organization, label: "FM organization", linked_only: true,
link_class: "admin-only bg-blue-100 text-sm #{eyebrow_link_class} px-2 py-1") %>
<%= link_to "Home", root_path, class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<%= link_to "Organizations", organizations_path, class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<%= link_to "Profile", organization_path(@organization), class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
Expand Down
3 changes: 1 addition & 2 deletions app/views/people/_admin_details.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@
<dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
<dt class="font-medium text-gray-500">FileMaker code</dt>
<dd class="text-gray-900">
<%= filemaker_code_links(person, archive: FmRolodex, table: "rolodexes",
link_class: "text-blue-600 underline hover:text-blue-800") || "β€”" %>
<%= filemaker_code_links(person, link_class: "text-blue-600 underline hover:text-blue-800") || "β€”" %>
</dd>
<dt class="font-medium text-gray-500">Racial/ethnic identity</dt>
<dd class="text-gray-900"><%= person.racial_ethnic_identity.presence || "β€”" %></dd>
Expand Down
7 changes: 1 addition & 6 deletions app/views/people/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -467,14 +467,9 @@
<div class="w-full sm:w-64">
<%= f.input :filemaker_code,
label: "FileMaker code",
hint: filemaker_code_hint(f.object),
input_html: { class: "w-full" },
wrapper_html: { class: "w-full" } %>
<%- if params[:admin] == "true" %>
<p class="mt-1 text-xs">
<%= filemaker_code_links(f.object, archive: FmRolodex, table: "rolodexes", label: "View archive",
link_class: "text-blue-600 hover:text-blue-800 underline") %>
</p>
<% end %>
</div>
</div>

Expand Down
6 changes: 2 additions & 4 deletions app/views/people/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,11 @@
<% if @person.user %>
<%= link_to "User account", user_path(@person.user), class: "admin-only bg-blue-100 text-sm #{eyebrow_link_class} px-2 py-1" %>
<% end %>
<%= filemaker_code_links(@person, label: "Rolodex", linked_only: true,
link_class: "admin-only bg-blue-100 text-sm #{eyebrow_link_class} px-2 py-1") %>
<% if allowed_to?(:index?, with: Admin::AhoyActivityPolicy) %>
<%= link_to "Activity", admin_activities_events_path(person_id: @person.id, time_period: "all_time", audience: %w[visitors users staff], hide_account: "1", hide_interactions: "1"), class: "admin-only bg-blue-100 text-sm #{eyebrow_link_class} px-2 py-1" %>
<% end %>
<% if params[:admin] == "true" %>
<%= filemaker_code_links(@person, archive: FmRolodex, table: "rolodexes", label: "Rolodex",
link_class: "admin-only bg-blue-100 text-sm #{eyebrow_link_class} px-2 py-1") %>
<% end %>
<%= link_to "Change password", change_password_path, class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<%= link_to "Home", root_path, class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
<%= link_to "People", people_path, class: "text-sm #{eyebrow_link_class} px-2 py-1" %>
Expand Down
15 changes: 15 additions & 0 deletions config/features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4221,3 +4221,18 @@
The FileMaker archive now sorts its 44 tables into labeled groups β€” people,
organizations, events, payments, funding and more β€” so the table you want
is easier to find.

- name: "Jump between people, orgs, and the FileMaker archive"
area: people
display_status: admin_facing
released_on: 2026-10-12
pr_number: 2648
summary: >-
Each FileMaker code on person and organization edit pages links to its
archive record, and each archive record links back to the portal person or
organization that carries its code.
pro_tips:
- "A struck-through code means the archive has no record for that code β€” check it for a typo."
- "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."
Loading
Loading