Skip to content

Expose school on people and position-during-role on project people (#1426) - #1427

Merged
jonfroehlich merged 3 commits into
masterfrom
1426-api-position-title-school
Jul 27, 2026
Merged

Expose school on people and position-during-role on project people (#1426)#1427
jonfroehlich merged 3 commits into
masterfrom
1426-api-position-title-school

Conversation

@jonfroehlich

@jonfroehlich jonfroehlich commented Jul 27, 2026

Copy link
Copy Markdown
Member

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_title but not the school), and 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 changes, no migration, and v1 stays additive-only.

What's new

GET /api/v1/people/<url_name>/ gains current_school and current_department alongside the existing current_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 / UMD rather than Professor / UW.

Three deviations from the fix proposed on the issue

1. Named position_*, not a bare title. The record already has a free-text role (the editor-written description of the work), so title sitting next to it read ambiguously. Full position_school ships 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._uic explicitly supports an undergrad who started a doctorate mid-grant) and roles that predate any recorded position. ProjectRole.position_during_role, in order:

  1. the latest-starting position whose range contains the role's start_date;
  2. else the latest position already begun by then (role started in a gap);
  3. else the earliest position (role predates every position — data drift, common in older imported roles);
  4. null only if the person has no positions at all — a real case here, since several external Sidewalk collaborators have a ProjectRole but no Position.

Ties on start_date break on pk: position_set has no Meta.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 prefetches position_set, which also fixes a pre-existing N+1 behind current_title. test_project_people_does_not_scale_queries_with_rows pins 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_abbreviated

Two 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:

  • One-word affiliations were acronymed to a single letter. It acronyms anything that isn't UW/UMD, so "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.
  • UW/UMD were matched on a bare substring. Any school with "washington" in its name got UW's initials — Washington State University, George Washington University, Washington University in St. Louis — and UMBC got UMD's. The special cases now match the phrase "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 distinct Position.school value 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, the pk tie-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.py keeps the wire contract: the fields are present and correct on the multi-position case, null for the no-position case, the two new person fields, and the query-count guard.
  • website/tests/test_ml_utils.py pins both abbreviation fixes plus the existing UW/UMD/UIC behavior, including UMD College Park still resolving to UMD.

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 role on the active Sidewalk people, and the grant whose grant_id is 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

jonfroehlich and others added 3 commits July 27, 2026 12:55
…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>
@jonfroehlich
jonfroehlich merged commit 7a08ec8 into master Jul 27, 2026
3 checks passed
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.

API: expose current school on people, and title/school-during-project on project people

1 participant