Skip to content

Denormalize the facilitator affiliation flag off the title - #2259

Closed
maebeale wants to merge 7 commits into
mainfrom
maebeale/facilitator-boolean-affiliations
Closed

Denormalize the facilitator affiliation flag off the title#2259
maebeale wants to merge 7 commits into
mainfrom
maebeale/facilitator-boolean-affiliations

Conversation

@maebeale

@maebeale maebeale commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 5 Inspect 🔬 denormalized flag on a core model, plus a migration and a post-deploy backfill that gates org Art Program status

What is the goal of this PR and why is this important?

  • "Is this a facilitator affiliation?" was a raw, collation-sensitive BINARY TRIM(title) = 'Facilitator' SQL scope.
  • The same rule was re-encoded in Ruby and copy-pasted across three JS controllers.
  • Four hand-written copies of one rule, one of them unindexable.

How did you approach the change?

Model

  • New affiliations.facilitator boolean (null: false, default: false).
  • sync_facilitator_from_title (before_validation) keeps it in step with the title: exactly "Facilitator", trimmed, case-sensitive.
  • Title stays the input and the single source of truth — a retitle re-syncs the flag.
  • .facilitators reads the flag; #facilitator? is the column's own reader.
  • Invariant: never write title via update_columns/update_all, which skips the callback and lets the flag drift.

JS

  • Rule extracted to lib/affiliation.js (isFacilitatorTitle); the three affiliation editors share it for their live preview.
  • Nothing facilitator-related is submitted from the form — the server owns the flag.

⚠️ Required post-deploy step

  • Run rake affiliations:backfill_facilitator immediately after deploy.
  • Until it runs, every existing row reads facilitator: false, so .facilitators matches nothing.
  • In that window Affiliation#sync_organization_status_with_affiliations (an after_save) sees no active facilitators and flips the org to Inactive via update_column, logging autochange.organization. Any affiliation save triggers it, including public event registration.
  • Open for review: folding the two UPDATEs into the migration's up closes the window.

Anything else to add?

  • Earlier revisions of this branch modeled the split as STI (FacilitatorAffiliation / JobAffiliation). Reverted: the subtypes needed model_name, policy_class, and an Ahoy event_resource_types indirection purely to keep behaving like one Affiliation.

@maebeale

maebeale commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

@jmilljr24 as i work more w the affiliations and org's art program status and org's active/inactive status, it'd really help to have the affiliation itself identified by more than the Title string on affiliation. you ok w adding this boolean?

i still want to do the same js style changes based on the title field, so this also includes drying up that logic bc it's used in more than one place rn.

@maebeale
maebeale requested a review from jmilljr24 August 19, 2026 13:08

@jmilljr24 jmilljr24 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this makes sense. Some of this will be surfaced again when membership gets fully built but I think a boolean to capture this on affiliation has been knocking on our door for a while.

@maebeale maebeale changed the title Add facilitator boolean to affiliations, synced from title Model facilitator vs job affiliations as STI subtypes Aug 19, 2026
@maebeale
maebeale force-pushed the maebeale/facilitator-boolean-affiliations branch from 67c33bc to afbe467 Compare August 19, 2026 14:00
maebeale added a commit that referenced this pull request Aug 19, 2026
Reading an affiliation's dates tells you what is true now but not how it got
there. The Ahoy lifecycle events already record every edit, and the trainings and
membership periods that explain those edits live in their own tables — this puts
all three in one time-ordered view so an admin can see why a row looks the way it
does without leaving the page.

Trainings and memberships are read from their own tables rather than from Ahoy:
Ahoy records *changes*, and only those made while a Current.user or Current.source
was set, so imported and seeded rows have no events at all. Ahoy is used only for
the affiliation's own columns, where nothing else records them.

Ahoy events are matched on every STI name for the row's table, not just the
record's current class. A row filed as Affiliation before the subtypes existed —
or under the other subtype before a retitle re-typed it — would otherwise lose
that history.

Split out of #2195; stacked on #2259 for the STI subtypes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
maebeale and others added 5 commits August 21, 2026 12:41
The "is this a facilitator affiliation?" fact was derived from a raw,
collation-sensitive BINARY TRIM(title) = 'Facilitator' scope re-encoded in
Ruby and three JS controllers. Denormalize it to a boolean column kept in
sync from the title, so the SQL scope reads a plain flag instead of raw SQL.
Title stays the input; the two-row (job + Facilitator) model is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the facilitator boolean with STI: FacilitatorAffiliation and
JobAffiliation (default). The subtype is derived from the title in a
before_validation, so title stays the single source of truth and a retitle
re-types the row. Server-authoritative — no form-submitted type needed.

Key STI accommodations: the type column has no default (a default subclass
name makes Affiliation.new build that subclass and break reload after the
callback re-types); #facilitator?/.facilitators read the type column;
subtypes share Affiliation's routes/param-key/dom_id via self.model_name and
authorize through AffiliationPolicy via self.policy_class.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ahoy stamps resource_type from the saving instance's class, so splitting
Affiliation into subtypes scattered a single row's history across three
names: "Affiliation" for everything recorded before the split (and for
freshly built rows), plus the two subtypes once a row is loaded back.
Both readers assumed one name, so the person History card silently
dropped affiliation edits and the affiliation edit page lost its
created-by/updated-by attribution.

Models now declare which names their events can carry, so the next STI
split doesn't have to rediscover this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@maebeale
maebeale force-pushed the maebeale/facilitator-boolean-affiliations branch from e6f321b to 75e1fb5 Compare August 21, 2026 16:43
maebeale and others added 2 commits August 22, 2026 04:53
The STI subtypes bought a typed model but cost a second source of truth for
every name the record answers to: model_name, policy_class and an
event_resource_types indirection for Ahoy all existed only to make two
subclasses keep behaving like one Affiliation. The boolean says the same
thing about the same rows without splitting the class.

Reverts da0d5e7..75e1fb5, restoring the tree from 1537201.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md requires AGENTS.md to track added rake tasks. Also drops the
duplicate migrate_sectors.rake line, which made the count read right by
accident.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@maebeale maebeale changed the title Model facilitator vs job affiliations as STI subtypes Denormalize the facilitator affiliation flag off the title Aug 22, 2026
@maebeale maebeale closed this Aug 22, 2026
@maebeale

Copy link
Copy Markdown
Collaborator Author

@jmilljr24 i talked myself out of this bc i'd still want to keep it simple for staff and therefore need that callback reconciliation on title. at that point, the boolean is just row-level caching and doesn't buy us much. def open to re-opening.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants