Expose school on people and position-during-role on project people (#1426) - #1427
Merged
Conversation
…1426) Project Sidewalk's new About page hydrates its team section from our public API. It couldn't render affiliations on the current-team cards (the API exposed current_title but not the school) or credentials in its ~150-person all-contributors roll call (the project-people endpoint exposed the free-text project role but not what the person *was* at the time). Everything needed was already on Position, so this is serializers + prefetching -- no model fields, no migration, and v1 stays additive-only. - PersonSerializer: add current_school and current_department, mirroring the existing current_title (all three are cached_property, not methods). - ProjectRoleSerializer: add position_title, position_school, and position_school_abbreviated -- the Position held when the role started, so a 2015 stint reads "Undergrad, UMD" even if that person is a professor today. Named position_* rather than a bare `title` so they don't read as part of `role`, the editor-written free-text description sitting next to them. - ProjectRole.get_position_during_role(): the resolution rule, documented -- the position containing the role's start date, else the latest one already begun (role started in a gap), else the earliest (role predates every position -- data drift), else None. Filters position_set in Python so it rides a prefetch, mirroring Person.get_latest_position. - Prefetch person__position_set on the project people/leadership endpoints and position_set on the people list, which also fixes the pre-existing N+1 behind current_title. A test pins query count as flat across roster size; without the prefetch it goes 11 -> 21 queries for 10 extra people. - ml_utils.get_school_abbreviated: stop acronyming one-word affiliations. Not every school is a university, and "Easterseals" -> "E" (or "Cornell" -> "C", which the local snapshot renders today) is worse than the name itself. This also fixes the public People page, which already shows these. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…UMD match Addresses review findings on #1426. - ProjectRole.position_during_role (renamed from get_position_during_role()) now breaks start_date ties on pk. position_set has no Meta.ordering, so two positions beginning the same day -- concurrent Member/Collaborator rows, or a duplicate entry -- let DB row order pick the winner, which could report a different title for the same role across identical requests. - It's a cached_property now, matching Person.get_current_title, so the API's three fields resolve it once per row from the model rather than via a private attribute stashed on the instance by the serializer. - get_school_abbreviated's UW/UMD special cases match the phrase "university of washington/maryland" instead of a bare substring. The substring check handed UW's initials to Washington State University, George Washington University, and Washington University in St. Louis, and UMD's to UMBC; the generic acronym path gets all four right on its own (WSU/GWU/UMBC), so they fall through to it. Punctuation is now normalized first, so a typed comma can't change the answer. Diffed old vs new across every school value in the local prod snapshot: no change except the two intended one-word fixes (Cornell, IIT-Delhi). - docs/API.md said the "nearest" position is used for a role starting in a gap; it's actually the most recent one already begun, which can be the further of the two. Corrected -- this is the public contract. - The resolution rules move to a new website/tests/test_project_role.py, unit tested directly against the model (all four rules, overlapping positions, the tie-break, and a zero-query assertion under prefetch), so a failure isolates to the logic. test_api.py keeps the wire-contract tests: fields present, nulls, and the N+1 guard. Full suite green: 640 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1426.
Project Sidewalk's new About page hydrates its whole team section from our public API so the roster never has to be hand-maintained. Two things it couldn't render: affiliations on the current-team cards (the API exposed
current_titlebut not the school), and credentials in its ~150-person all-contributors roll call (the project-people endpoint exposed the free-text projectrole, but not what the person was at the time).Everything needed was already on
Position, so this is serializers + prefetching — no model changes, no migration, andv1stays additive-only.What's new
GET /api/v1/people/<url_name>/gainscurrent_schoolandcurrent_departmentalongside the existingcurrent_title.GET /api/v1/projects/<short_name>/people/gains three fields:{ "person": { ... }, "role": "Led the accessibility audit tooling", "lead_project_role": "Student Lead", "position_title": "Undergrad", "position_school": "University of Maryland", "position_school_abbreviated": "UMD", "start_date": "2015-06-01", "end_date": "2017-05-31", "is_active": false }These are what the person was when they joined the project, not what they are now — that's the whole point, since a 2015 undergrad's profile otherwise shows whatever they do today. Verified against a prod snapshot: Jon's 2012 Sidewalk role now reports
Assistant Professor/UMDrather thanProfessor/UW.Three deviations from the fix proposed on the issue
1. Named
position_*, not a baretitle. The record already has a free-textrole(the editor-written description of the work), sotitlesitting next to it read ambiguously. Fullposition_schoolships alongside the abbreviation so the consumer can pick per context (cards vs. the dense roll call). The naming is confirmed on the issue so the Sidewalk side can make its one-line change.2. "The position overlapping the start" needed an explicit rule. Real data has multi-position people (
seed_sidewalk_participants._uicexplicitly supports an undergrad who started a doctorate mid-grant) and roles that predate any recorded position.ProjectRole.position_during_role, in order:start_date;nullonly if the person has no positions at all — a real case here, since several external Sidewalk collaborators have aProjectRolebut noPosition.Ties on
start_datebreak onpk:position_sethas noMeta.ordering, so without that a person holding two positions that begin the same day (concurrent Member/Collaborator rows, or a duplicate entry) would have the winner picked by whatever order the DB happened to return — the same role could report a different title from one request to the next.3. N+1. Resolving a position per role in the serializer is a query per row — ~150 on the roll call. Both project sub-resources now
prefetch_related("person__position_set"), and the people list prefetchesposition_set, which also fixes a pre-existing N+1 behindcurrent_title.test_project_people_does_not_scale_queries_with_rowspins the count as flat across roster size; I confirmed it actually bites by temporarily dropping the prefetch — 11 → 21 queries for 10 extra people.Drive-by fixes to
get_school_abbreviatedTwo bugs in the same helper, both already live on the public People page and both newly load-bearing now that the API serializes this value:
"Easterseals"→"E"; the local prod snapshot renders Cornell as"C"and IIT-Delhi as"I"today. A name that would acronym to a single letter is now returned unchanged."university of washington"/"university of maryland", and everything else falls through to the acronym path, which happens to get all four right on its own (WSU, GWU, UMBC). Punctuation is normalized first, so a typed comma can't change the answer."University of Illinois Chicago"→"UIC"is unaffected. I diffed old vs. new output across every distinctPosition.schoolvalue in the local prod snapshot: no change except the two intended one-word fixes (Cornell, IIT-Delhi).On the UI side, this touches text only. All 15 current-member cards (
.person-card-affiliation) are UW, so they render identically; the two changed strings land in.past-member-affiliation, an inline span in a wrapping list ("C CS, 2015"→"Cornell CS, 2015"), with no fixed-width container involved. No layout or a11y change, so no Pa11y run.Testing
Full suite green (640 tests).
website/tests/test_project_role.py(new) unit-tests the resolution rules directly against the model: all four rules, overlapping positions, an open-ended position, thepktie-break, and a zero-query assertion under prefetch. A failure there points at the logic rather than at the API contract.website/tests/test_api.pykeeps the wire contract: the fields are present and correct on the multi-position case,nullfor the no-position case, the two new person fields, and the query-count guard.website/tests/test_ml_utils.pypins both abbreviation fixes plus the existing UW/UMD/UIC behavior, including UMD College Park still resolving toUMD.Docs updated in
docs/API.md. Version bumped to 2.28.0 — minor rather than patch because this adds public API surface, which is exactly what the "v1 fields are additive-only" contract signals with a minor bump.Not in this PR
The issue's data-entry notes are data, not code: the blank per-project
roleon the active Sidewalk people, and the grant whosegrant_idis the literal string"None". Both are admin fixes — better to fix that one row than teach the serializer to special-case the string.🤖 Generated with Claude Code