Make the profile the source of truth for author credit - #2093
Conversation
| end | ||
| end | ||
|
|
||
| context 'via search_by_params (the user-facing path)' do |
There was a problem hiding this comment.
🤖 From Claude: Behavior change worth knowing: an author name can no longer be one term of an AND full-text query (search("John technology") used to work). Honoring the credit preference needs per-person branching that a flat SearchCop index cannot express, so person-name search moved entirely to by_credited_person_name, OR-ed in by search_by_params. Single-term author search still works.
| <%= link_to divergence_record_title(record), polymorphic_path(record), | ||
| target: "_blank", rel: "noopener", | ||
| title: "Opens in a new tab", | ||
| class: "text-blue-700 hover:underline" %> |
There was a problem hiding this comment.
🤖 From Claude: Deliberate deviation from the eyebrow convention. These link to 8 different destinations, none of which has a return_to mechanism today (they use hardcoded utility-link rows), so retrofitting all eight was out of scope here — using target="_blank" instead. Happy to do the eyebrow work as a follow-up if you would rather link in-page.
| end | ||
| end | ||
|
|
||
| context 'via search_by_params (the user-facing path)' do |
There was a problem hiding this comment.
🤖 From Claude: Behavior change worth knowing: an author name can no longer be one term of an AND full-text query (search("John technology") used to work). Honoring the credit preference needs per-person branching a flat SearchCop index cannot express, so person-name search moved entirely to by_credited_person_name, OR-ed in by search_by_params. Single-term author search still works.
| <%= link_to divergence_record_title(record), polymorphic_path(record), | ||
| target: "_blank", rel: "noopener", | ||
| title: "Opens in a new tab", | ||
| class: "text-blue-700 hover:underline" %> |
There was a problem hiding this comment.
🤖 From Claude: Deliberate deviation from the eyebrow convention. These rows link to 8 different destinations, none of which has a return_to mechanism today (they use hardcoded utility-link rows), so retrofitting all eight was out of scope — using target="_blank" instead. Happy to do the eyebrow work as a follow-up if you would rather link in-page.
| <%= link_to divergence_record_title(record), polymorphic_path(record), | ||
| target: "_blank", rel: "noopener", | ||
| title: "Opens in a new tab", | ||
| class: "text-blue-700 hover:underline" %> |
There was a problem hiding this comment.
🤖 From Claude: Deliberate deviation from the eyebrow convention. These rows link to 8 different destinations, none of which has a return_to mechanism today (they use hardcoded utility-link rows), so retrofitting all eight was out of scope — using target="_blank" instead. Happy to do the eyebrow work as a follow-up if you would rather link in-page.
bdcfd5d to
c1d2886
Compare
a13d917 to
11fc630
Compare
11fc630 to
e4c6167
Compare
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keeps the admin worklist open while checking a profile, matching the content links that already open in a new tab. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clearing a single column is what makes that column safe to drop, so each one gets its own subsection and its own all-clear note naming the field. Guess who each free-text name refers to so an admin confirms rather than looks every one up, resolved in one query for the page. Co-Authored-By: Claude <noreply@anthropic.com>
Every fix on the author credit divergences page (Save stored consent, Apply to profile, Credit to a person) did a full-page redirect. Respond with a Turbo Stream that re-renders just the results frame and flash, so the worklist updates in place with no page flip. Non-Turbo requests still redirect. Also widen the remote-select left padding so the search icon never overlaps the placeholder or selected value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- author_credit returns "Anonymous" for a stored-anonymous item even when it carries a legacy free-text name — that name belongs to no profile, so nothing else would suppress it. - Add a credited_openly scope so a NULL snapshot (which means "follow the profile") stays visible; a bare where.not would drop it, since NULL never compares unequal. Use it for a person's public authored content. - Build the divergences result only on the frame request, so the full page load stays cheap. - Under an active filter, an empty section is the filter's doing, not a milestone, so section-clear withholds the congratulations and cleanup note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The divergences page couldn't finish its own job. An item whose stored consent was "anonymous" offered no way back to "follow the profile", so the only exit was picking a name format — re-recording a consent nobody gave. Per-item anonymity is the legacy state this page exists to drain, and the profile is the source of truth, so clearing it is now allowed on every row. The author picker fell back to the creator, so a workshop credited to the legacy name "Lisa Cohen" showed "Umberto User" in its own edit form — and the next save would have written that creator into author_id, destroying the legacy credit. The picker now reflects only the record's own author (still defaulting to the creator on new records) and names the legacy credit standing in when there isn't one. Sorting COALESCEd author, creator, legacy while display used author, legacy, creator, so legacy-credited rows sorted under a name they never show. A note under each author picker flags when the credited person's profile suppresses credits, since nothing on the record itself reveals that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the creator The column reads better as the state it is than as a predicate on a collection, and it now matches the "Anonymous contributions" label already used in the profile form and the factory trait. The migration is unmerged, so it changes in place rather than stacking a rename on top of itself. A legacy free-text name follows nobody's profile, so a legacy-credited row now reports no governing person. It was being grouped on the divergences page under whoever happened to enter it, asserting that person's profile had drifted from a consent snapshot that never described them. Those rows have to be matched to a real person by hand against the legacy text — the creator is not a fallback for them. Community news author is optional on the model, so the form offers a blank option rather than forcing whoever edits one of the legacy authorless rows to credit somebody before they can save anything else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The table had drifted well past this branch's additions. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The workshop show page rendered a Resource's author association directly, which has no to_s — so a public page printed nothing where a credit belonged, honoring neither the name preference nor anonymity. The workshop idea page used User#full_name, the real name, where its sibling idea pages use User#name. Also drops the obsolete brakeman ignore entry for callouts_controller; the warning it silenced no longer fires. Co-Authored-By: Claude <noreply@anthropic.com>
… fallback Two paths still routed a legacy free-text credit through its creator. The consent snapshot was taken from the creator's profile on create, so a legacy row was born diverged from a profile that never described it — the exact rows that shouldn't be resolved that way. And credited-name search matched the creator, so a workshop displaying "Lisa Cohen" came back under the name of whoever typed it in, which is the mismatch between search and display the rest of this work removes. The portal sits behind a login, so an unattributed credit says "AWBW Facilitator" rather than "AWBW Staff" or "Anonymous". "Anonymous" in particular read as a deliberate privacy choice when it only ever meant nobody was recorded. That makes every per-model override redundant, so they come out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entering a record isn't claiming it, so a model that can name an author now credits only that author. Where none is named the credit reads "AWBW Facilitator" rather than the name of whoever typed it in — and it's no longer findable or sortable under that person either, which was the last way the creator leaked into a credit they had nothing to do with. The idea models keep their creator credit: they have no author_id at all, so it's the only attribution they can carry. A suppressed credit reads "AWBW Facilitator" too, from either anonymity path. Behind a login, "Anonymous" invited the reader to wonder who was being hidden; the generic credit just says the org stands behind it. The suppression is unchanged — the name is still withheld and still never links. Section 3 of the divergences page follows: those records show the generic credit now, so the creator appears there as a suggested author to confirm, not as the profile that formats them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The credit notice sat inside the admin-only block on this form, alongside the staff notes and the created-by picker, so the one person who needs it never saw it. Story ideas and variation ideas already showed it outside that gate. Covers all three idea forms with the assertion that was missing: the notice states the profile's current answer and points at contact us, rather than asking the submitter to pick a preference per submission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The profile's "Submitted content" section and its Turbo-Frame section endpoints were gated on `show?` — the same rule about to widen so people can view profiles. Widening it would expose everyone's private submitted ideas and workshop logs. Split off a narrow `own_record?` (admin || owner) and gate the private sections and their endpoints on it, so `show?` can open up for public viewing without leaking private content. Also gate the "Create user" admin control behind the user create policy instead of an `admin-only` CSS class that doesn't actually hide anything. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Say plainly where each preference reaches: display name is sitewide, while anonymous contributions only hide the author credit on content and never touch the people index or profile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stored author_credit_preference is the submitter's consent snapshot, so it now wins the display format over the credited person's current profile (falling back to the profile only when the record stored nothing). Search and sort follow the same precedence, so neither can surface a name the credit hides. An unattributed record — including every idea model, which has no author_id — falls to the generic "AWBW Facilitator" rather than crediting whoever entered it; entering a record isn't claiming authorship. The divergences page still surfaces those with the creator suggested as the author to assign. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Matches the person form, so the same control reads the same in both places. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the profile's current credit out of the header to sit left of the "Set profile to" select, reading current → new, and pull the suggestion under that select. The read-only current state makes clear the Apply button changes only the new preference and the anonymity toggle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Credit split by whether there's an author: an unattributed record now reads "AWBW Staff" (org content nobody claimed), while an author who opted anonymous reads "AWBW Facilitator" (a facilitator, unnamed). "No author" wins over anonymity, so an anonymous record with only a legacy name and no person also reads Staff. Divergences page: - A collapsed "Reconciled" section at the bottom lists everyone already resolved. - Item links open the record's edit page, not its show page. - The legacy section sorts by model, then legacy name. - The person card shows the current format and anonymous status (kept on one line) and reads current → new, with Apply scoped to the new fields; dropped the stale "only Anonymous renders" note and the creator suggestion caption. - The record edit warning hides once the person is reconciled, and states that the item keeps rendering by its stored preference until then. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The select and the anonymity checkbox already default to the suggested value, so the caption just repeated the control's own state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Saving a divergence fix re-renders the results over Turbo, but only rebuilt the worklist — the reconciled section vanished because its data wasn't reloaded. Share one loader between the index and the update response so both stay whole. Also: rename the "Renders as" column to "Credit display", put the profile-format select and the anonymity checkbox on one row under "Set profile to", and trim the legacy and no-author section descriptions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Four views spelled out "AWBW Facilitator" / "AWBW Staff" by hand, so a change to either label would leave them quietly lying. Promote both to constants on AuthorCreditable; views with a record in hand go through anonymous_author_label / missing_author_label so a model can still override, and the rest reference the constants. Co-Authored-By: Claude <noreply@anthropic.com>
One for facilitators explaining how attribution resolves and who can see what, one admin-facing for the divergences worklist. Co-Authored-By: Claude <noreply@anthropic.com>
Per-item anonymity is a legacy state the divergences page exists to drain, not something facilitators choose — so the facilitator-facing entry describes the profile setting covering all their contributions, and the admin entry says where this is headed. Co-Authored-By: Claude <noreply@anthropic.com>
Facilitators shouldn't be told about a transitional state they didn't choose and can't act on. Co-Authored-By: Claude <noreply@anthropic.com>
The directory counts on main are stale for reasons unrelated to this work, and rewriting them here has conflicted on every rebase. Left them alone; kept the concern description and the new service entry. Co-Authored-By: Claude <noreply@anthropic.com>
c8995d2 to
6fca92e
Compare
🤖 suggested review level: 5 Inspect 🔬 changes how every author credit renders app-wide, narrows search results, and adds an admin reconciliation page
Name display was asked in two unrelated places with two different value sets —
people.display_name_preference(people index) and<item>.author_credit_preference(every credit, on 8 forms).AuthorCreditabledeliberately calledperson.full_namerather thanperson.name, so the per-item answer won and the profile was ignored. One facilitator could render three different ways with no single place to fix it.Now: the profile formats every credit. The 8 per-item selects come off. The stored column is retained as the consent record, human-editable only on a new admin reconciliation page.
Two axes on the profile
display_name_preference(4 values)Person#nameand author creditsanonymous_contributionsanonymous_contributionsalready existed on main (added by20260810225127) but was inert: nothing read it. This PR makes it actually govern credits, soStoryImporterflagging an anonymous import now genuinely suppresses that credit.The one asymmetry
A stored
author_credit_preferenceno longer drives display — exceptanonymous, which is always honored. Anonymity is true if the profile says so or the item says so; neither can strip it from the other. A suppressed credit renders the generic "AWBW Facilitator", not the word "Anonymous" — behind a login that invited the reader to wonder who was being hidden, where the generic credit just says the org stands behind it. The suppression itself is unchanged: the name is withheld and never links. Anonymity is inherently per-item (a person may want four stories credited and the fifth not), admins need a per-item lever, and there is no backfill — so without this, every item currently storinganonymouswould start rendering its author's real name on deploy.Search honors the preference
Anonymous → matches nothing.
first_name_only→ not findable by last name.first_name_last_initial→ matches the initial only. This also meant dropping theperson_first/person_lastSearchCop attributes onStory/CommunityNews, which bypassed the gate. Tradeoff: an author name can no longer be one term of an AND full-text query — honoring the preference needs per-person branching a flat index can't express.Reconciliation page (
/author_credit_divergences, linked from admin home)Four sections, in the order to work them. Each names the code it retires once cleared, so the legacy credit paths can actually be deleted:
workshops.full_name,resources.legacy_author_name) so each field can be retired on its own, with a suggested person matched from the free-text nameauthor_id, so the credit falls back to the creating user's person (idea models excluded — that's their only credit path)Sections 2–4 all resolve by assigning an
author_id, the only credit path that follows a profile, links to it, and lists the record there. Filterable by person (remote search), content type, stored preference, and reconciled state.Only the author is credited
Entering a record isn't claiming it, so a model that can name an author credits only that author — no creator fallback in display, search, or sort. Where none is named the credit reads "AWBW Facilitator". The idea models are the exception: no
author_idcolumn at all, so their creator is the only attribution they can carry, andcredits_creator?draws that line in one place. Section 3 of the reconciliation page follows — those records already show the generic credit, so the creator is offered there as a suggested author to confirm, not as the profile that formats them.Legacy credits stay clear of whoever entered them
A legacy free-text name outranks the creator, so those rows have no governing person. They take no consent snapshot on create (they were being born diverged from a profile that never described them), they aren't grouped under their creator in section 1, and credited-name search no longer returns them under the creator's name — a workshop displaying "Lisa Cohen" was coming back under the name of whoever typed it in. They resolve only by matching a real person against the legacy text. The author pickers follow the same rule: they show the record's own
author(defaulting to the creator on new records only), so saving an untouched form can't silently promote the creator over a legacy credit.One fallback label
An unattributed or suppressed credit reads "AWBW Facilitator" everywhere. The portal is behind a login, and the old three-way split ("AWBW Staff" on community news/resources, "AWBW Facilitator" elsewhere, "Anonymous" on the idea models) made "Anonymous" read as a deliberate privacy choice when it only ever meant nobody was recorded. All five per-model overrides are gone.
Public submitters are told, not asked
The idea forms' "how would you like your name published?" select is gone. In its place each of the three forms states the profile's current answer ("You'll be credited as …") and links to Contact us. On the workshop idea form that notice was rendering inside the admin-only block, next to the staff notes, so the submitter never saw it — now it sits outside the gate like the other two.
Also swept
Four places where a real name escaped past an anonymized credit: the variation-ideas index link, the story form's subtitle,
Workshop#author_name, and two deadResourceDecoratormethods. Plus two phantom strong params (profile_show_name_preference,promoted_to_story) referencing columns that don't exist.Verified
Full suite green (6907 examples, 0 failures, 17 pre-existing pendings) via
ai/test_extra. Rubocop clean on 1511 files. Driven in the running app: format changes propagate to the index and every credit at once, both anonymity paths suppress credit and link, search gating confirmed against real data, all four reconciliation sections populate and resolve.🤖 Generated with Claude Code