Skip to content

JM: WIP: Profile change requests with an admin review queue - #2543

Open
maebeale wants to merge 7 commits into
mainfrom
maebeale/person-data-change-requests
Open

maebeale wants to merge 7 commits into
mainfrom
maebeale/person-data-change-requests

Conversation

@maebeale

@maebeale maebeale commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

🤖 suggested review level: 5 Inspect 🔬 new model + migrations, admin apply-on-approve logic, notification wiring, lazy queue + turbo-stream actions

Facilitators can't edit their primary email, organization name, or affiliation details — this gives them a way to request those corrections and gives admins an in-app queue to act on, instead of the changes living only in an email.

Owner side

  • On their own profile, each locked field has a "Request a change" link → a per-field form.
    • Primary email / organization name: shows the current value, takes the new value.
    • Affiliation: pick a specific affiliation (or "a new one"), choose what should change (title/role, dates, organization, remove, add, other), and give details — structured, not free prose.
  • Single-target fields (email/org) allow one open request and the link reflects a pending state ("Change requested — pending review · View your request"), re-opening it to edit. Affiliation requests are per-affiliation, so several can be open at once.
  • Submitting creates a ProfileChangeRequest and the admin + submitter notification email thread.

Admin side

  • Changes requested queue (/profile_change_requests) — lazy turbo-frame with status filter, linked from the admin home (pending count) and surfaced on each person's edit page. Cards show the target affiliation.
  • Actions answer with turbo-streams (row updates in place): Approve (auto-applies where it can via ProfileChangeRequests::Apply), Update manually (deep-links to the exact affiliation), Mark as resolved, Decline. Resolve/Decline carry a reviewer note.
  • Reviewing notifies the requester (profile_change_reviewed).

Safeguards

  • Email approve rejects an address already used by another account; email/org approve no-op when the value already matches.
  • Org rename targets a snapshotted organization and shows a blast-radius confirm.
  • One open request per target (per field for email/org, per affiliation for affiliations); requested_value required on every request.

Notes for reviewers

  • Owner reachability rides the staged OWNER_PROFILE_EDIT flag; decoupling that entry point is deferred to the profile-launch work.
  • Affiliation changes aren't auto-applied (free-form) — admin edits the pinned affiliation via "Update manually", then marks resolved.

🤖 Generated with Claude Code

@maebeale maebeale changed the title Let profile owners request changes to admin-only fields Profile change requests with an admin review queue Sep 19, 2026
@maebeale
maebeale force-pushed the maebeale/person-data-change-requests branch from 51c5056 to 88e0a12 Compare September 20, 2026 23:25
@maebeale
maebeale marked this pull request as ready for review September 21, 2026 02:29
@maebeale
maebeale force-pushed the maebeale/person-data-change-requests branch from 88e0a12 to 901891a Compare September 21, 2026 02:31
@maebeale maebeale changed the title Profile change requests with an admin review queue JM: Profile change requests with an admin review queue Sep 21, 2026
@maebeale
maebeale requested a review from jmilljr24 September 21, 2026 03:40
@maebeale maebeale changed the title JM: Profile change requests with an admin review queue JM: WIP: Profile change requests with an admin review queue Sep 21, 2026
maebeale and others added 7 commits September 21, 2026 00:36
Non-admin owners can't edit their primary email, organization name, or
affiliation details, but had no way to ask for a correction. Add a reusable
"Contact us to request a change" affordance (prefilled through the existing
ContactUs → admin notification channel) next to those read-only fields, and
stage owner self-service profile editing behind Person.owner_editing_enabled?
(OWNER_PROFILE_EDIT) so it can be trialed in staging before the profile-launch
policy flip. The controller strips the locked fields from an owner's submission
as a server-side backstop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the email-only "request a change" links with a ProfileChangeRequest
record so admins can act on requests in-app instead of only over email. Owners
submit a per-field request from their profile; submitting still creates the
admin + submitter notification email thread, now pointing at the structured
record. Admins work a "Changes requested" queue (linked from the admin home and
surfaced on each person's edit page): Approve auto-applies where it can (primary
email via the existing confirmation flow, organization rename), otherwise Update
manually + Mark as resolved, or Decline. Status is pending/resolved/declined with
a resolution_method (approved/manual).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round of improvements to the change-request feature:
- Notify the requester when a request is resolved or declined, with an
  optional reviewer note (required-in-spirit on decline) shown to them.
- One open request per (person, field): the "Request a change" link re-opens
  the pending one to edit, and owners can edit their own pending request.
- Snapshot the target organization/affiliation so a later profile change can't
  move the target; org rename now shows a blast-radius confirm.
- Apply guards: reject an email already used by another account, and no-op
  (resolve without re-sending) when the value already matches.
- Admin queue is now a lazy turbo-frame with status filtering; Approve /
  Decline / Mark-as-resolved answer with turbo-streams so the row updates
  in place instead of a full reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Aligns with the See Other convention from #2536, picked up in the rebase
onto main, so a Turbo form submission advances instead of re-rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Affiliation requests were a single free-text box. Now the requester selects
which affiliation (or "a new one"), picks what should change (title/role, dates,
organization, remove, add, other), and gives details — so staff get a specific,
structured request instead of prose to parse.

- affiliation_id target (validated to belong to the person); admin "Update
  manually" deep-links to that affiliation, and the card shows which one.
- One open request per affiliation (not just per field), so two affiliations can
  be flagged at once; the pending-state link still applies to the single-target
  email/org fields.
- requested_value is now required for every field (the new value / the category).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Affiliation requests now capture the concrete new value per category, so Approve
applies them like the email/org requests instead of always needing a manual edit:

- Category-driven form (reuses the conditional-fields Stimulus controller): title
  → text; dates → two date fields; organization → new name; add a new affiliation
  → org search (remote-select) + title + dates; remove → none; other → details.
- Approve applies title/dates/org-rename/remove(end-date+inactivate)/add-new;
  "Other" stays manual. Org rename keeps the blast-radius confirm.
- proposed_* columns store the new value; irrelevant sibling values are cleared
  before save; per-category presence validations.
- Admin card shows the proposed "New value"; conditional-fields now accepts a
  comma-separated show-when so one field can serve several categories.

Also: status chips moved to the left of affiliation rows; "Remove this
affiliation" wording; line break in the request intro.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maebeale
maebeale force-pushed the maebeale/person-data-change-requests branch from 882d077 to f446e5b Compare September 21, 2026 04:37

@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 like how this is setup.

The change request flag is not showing on affiliation change like the others. The validation does work though if a person goes in and tries to make another change when one is still pending.
Image
No route for this noticiable.
Image

# and affiliations are rendered read-only for owners, who can only *request*
# changes to them. Drop those from a non-admin owner's submission so a crafted
# request can't slip past. No-op for admins, who edit them directly.
def reject_owner_locked_changes!(attrs)

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.

Can we use action policy scoped params?

end

def update
authorize! @request, to: :update?

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.

Suggested change
authorize! @request, to: :update?
authorize! @request

end

def edit
authorize! @request, to: :update?

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.

Suggested change
authorize! @request, to: :update?
authorize! @request

I'd either add a policy action for edit or use alias in the policy.

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.

Same comment for all the controller actions in this file.

locals: { request: @request }
)
end
format.html { redirect_back fallback_location: profile_change_requests_path, notice: notice }

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.

Suggested change
format.html { redirect_back fallback_location: profile_change_requests_path, notice: notice }
format.html { redirect_back_or_to profile_change_requests_path, notice: notice }

redirect_back is soft deprecated.

Comment on lines +114 to +120
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
ActionView::RecordIdentifier.dom_id(@request),
partial: "profile_change_requests/request",
locals: { request: @request }
)
end

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.

Suggested change
format.turbo_stream do
render turbo_stream: turbo_stream.replace(
ActionView::RecordIdentifier.dom_id(@request),
partial: "profile_change_requests/request",
locals: { request: @request }
)
end
format.turbo_stream

If you create a turbo_stream.erb dom_id is available without calling ActionView....` and its a bit more idiomatic

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.

Also you will want to add flash.now if you want the same notice that you are using for html response

Comment thread app/models/person.rb
# Owner self-service profile editing is staged behind this flag so it can be
# trialed in staging before PersonPolicy#edit? flips from admin-only to
# admin-or-owner at profile launch. Default off; set OWNER_PROFILE_EDIT=true.
def self.owner_editing_enabled?

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'm not apposed to using an env but just a thought.

We have !Rails.env.production? for enable? on membership. We could do the same for this. On less thing to do on DO

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