rubyforgood / rubyforgood/awbw
Person timeline: normalize PaperTrail changesets across create/update/destroy at read time
Nobody has claimed this yet.
- Dominant language
- Ruby
- Stars
- 15
- Forks
- 26
- Avg merge
- 12h 42m
- Merged PRs (30d)
- 242
Description
Context
We're versioning financial/membership/registration models with PaperTrail (subject_person_id meta) to build a person-scoped timeline. PaperTrail stores diffs asymmetrically by event, so a naive reader has to special-case events:
| event | object (full state) |
object_changes (diff) |
|---|---|---|
| create | nil | {field: [nil, new]} |
| update | prior state | {field: [old, new]} |
| destroy | final state | nil |
Destroy events never populate object_changes; the final state lives only in object.
Decision 1 — normalize changesets at read time
Do not force object_changes onto destroy at write time — it duplicates data already in object, is a semantic fiction ([value, nil]), fights PaperTrail defaults, and only half-fixes the asymmetry (create's object is still nil).
Instead, normalize at read time with a single helper that returns a uniform {field => [before, after]} for any event, deriving destroy's diff from the object snapshot. One tested place; stored data stays honest and minimal.
def timeline_changeset(version)
case version.event
when "destroy"
version.object_deserialized.transform_values { |v| [v, nil] }
else
version.changeset # create & update already give [before, after]
end
end
Decision 2 — actor (whodunnit) is read-time only, no schema change
whodunnit is native PaperTrail (the built-in column — t.string "whodunnit" in db/schema.rb — plus set_paper_trail_whodunnit / PaperTrail.request.whodunnit / version.whodunnit). It already holds the acting user's id, per row, so "changed by" is available straight from the versions table (distinct from subject_person_id, which is the subject).
- No label snapshot. The only strong case for freezing an actor label was data loss on user deletion — but users with history won't be deleted, so that's moot. A snapshot would also go stale on name changes.
- No new
user_idcolumn.whodunnitalready carries the id. - Just batched-preload users at read time to kill the current N+1 (
User.find_byper row in the partial):
ids = versions.map(&:whodunnit).compact.map(&:to_i)
users = User.where(id: ids).index_by(&:id)
# per row: users[version.whodunnit.to_i]
Wart: whodunnit is a String, not a typed FK, so no includes(:user) and a .to_i cast is needed. Only if that ergonomics gets in the way, add a typed user_id meta later — a nicety, not a correctness need.
Scope
- Decide where the normalizer lives (Version decorator / timeline PORO / helper)
- Route the new person-timeline reader through it
- Route the existing
app/views/application/_papertrail_versions.html.erbpartial through it (currently skips destroy diffs entirely:version.event != 'destroy'— should show "everything removed") - Replace the actor N+1 in that partial (
User.find_by(id: version.whodunnit)per row) with a batched preload - Handle nil
whodunnit(system / console / seed / rake / background-job actions) — display as "System"/"Unknown" and account for it in any actor filter - Spec create/update/destroy cases
Notes
- Serialization is YAML (no PaperTrail initializer → defaults).
object_changesis auto-enabled because theobject_changescolumn exists onversions.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with app/views/application/_papertrail_versions.html.erb and the existing person-timeline reader, then trace how PaperTrail versions are loaded and rendered. Decide where the shared normalizer belongs, route both readers through it, batch-load whodunnit users while handling nil actors, and add specs covering create, update, and destroy events.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- ruby
- Domain
- backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100