Skip to content

Add UW grant tracking: worktag/award fields + admin links to the official UW trackers (#1448) - #1449

Merged
jonfroehlich merged 3 commits into
masterfrom
1448-uw-grant-tracking
Aug 12, 2026
Merged

Add UW grant tracking: worktag/award fields + admin links to the official UW trackers (#1448)#1449
jonfroehlich merged 3 commits into
masterfrom
1448-uw-grant-tracking

Conversation

@jonfroehlich

@jonfroehlich jonfroehlich commented Aug 12, 2026

Copy link
Copy Markdown
Member

Closes #1448.

Two admin-only additions around grants, sharing one privacy boundary.

1. UW internal tracking codes on each grant

Three nullable fields on Grant, in a new UW Internal Tracking fieldset:

field what it is
uw_grant_id the UW/Workday grant worktag (GR…); comma-separate when an award spans several
uw_award_number UW's award number (AWD-…)
uw_award_name the award name as UW records it, which is often not our title

All three are searchable — pasting a worktag out of an email into the search box
is the main way this page gets used — and the worktag sits right beside the title
in the changelist.

These never leave the admin. GrantSerializer uses an explicit field
allowlist, so they're excluded structurally; test_api.py now pins that, both
per-field and by scanning the whole response body for a worktag.

Naming

Grant.grant_id already existed and means the sponsor's award ID (the NSF
number). It's public and stays in the API untouched — hence the uw_ prefix on
the new field. It gains a verbose_name of "Sponsor grant ID" so the two are
told apart on the form.

2. Links to the official UW trackers

A GrantTrackingLink model (label / url / notes / display order) rendered as a
link bar atop the Grant changelist. These are database rows, not code: the
real URLs embed per-PI SharePoint sharing tokens and this repo is public. Same
reason the URLField is max_length=1000 — the real UW CSE SharePoint link is
425 characters, so Django's 200-char default would reject it outright
(verified against the actual URL locally; there's a regression test).

They must be entered once per environment via Grants & Funding → Grant
tracking links
; the panel shows an "add one" hint until they are.

Permissions change

Editors (PhD students, long-term staff) now hold view_grant — read-only —
so they can look up a worktag for a purchase, trip, or appointment without
pinging the PI. Nothing more. Two audiences, two views:

Editors (PhD students) Superuser
changelist columns Title, UW Grant ID, Sponsor, Date + First Author, Funding Amount
tracking link bar hidden shown
Total Funding rollup hidden shown
change form read-only, no save buttons editable
funding amount / proposal files hidden shown
Grant tracking links admin 403 full

So the #1125 decision that funding data stays with the superuser still holds.
Contributors get nothing. GrantTrackingLink is superuser-only by the same
mechanism as Award — absent from both group specs.

EDITOR_LIST_DISPLAY is an allowlist rather than the superuser list with
sensitive columns filtered out, so a column added for superusers later can't leak
into the PhD-student view by omission. Relatedly, prefetch_related('authors')
(the #1346 fix for the first-author column) now applies only when that column is
actually rendered — Editors don't get it, so they don't pay for the join.

One Django subtlety worth knowing: read-only rendering resolves labels through
admin.utils.label_for_field, which reads the model's verbose_name and
ignores ModelAdmin form labels. So the labels live on the model — that's what
makes them correct on the read-only page Editors actually see. A test guards it.

Testing

  • New website/tests/test_grant_tracking.py (18 tests): optional fields,
    long-URL round-trip, link ordering, the per-audience column lists, the prefetch
    behavior, and the full superuser-vs-Editor-vs-Contributor visibility matrix on
    both the changelist and the change form.
  • test_api.py: the uw_* omission guards.
  • test_setup_admin_groups.py: the Grant/Award/GrantTrackingLink boundaries,
    rewritten to compare exact codenames (add_granttrackinglink contains the
    substring _grant, so the old substring assertion would have been misleading).
  • Full suite: 789 tests, 785 pass. The 4 failures (test_logging_config,
    test_backup_status) are pre-existing and environmental — verified identical on
    a stashed baseline; the local image predates concurrent-log-handler.
  • QA'd by hand on localhost as both a superuser and a real Editors-group account.
  • Pa11y not run: this change touches no public-facing template, only admin pages,
    which aren't in .pa11yci.json's scan set. The panel is colored from the
    admin's CSS custom properties so it follows the light/dark theme toggle (it had
    been hard-coded #f8f9fa, which rendered light-on-light in dark mode).

Deploy note

No migration to commit (website/migrations/ is gitignored); the entrypoint's
makemigrations/migrate creates the three columns and the new table on each
environment. Locally it generated 0044_granttrackinglink_grant_uw_award_name_and_more
and 0045_alter_grant_grant_id cleanly.

After merging and tagging, add the two tracking links in prod's /admin — they're
data, not code, so they don't ride along with the deploy.

🤖 Generated with Claude Code — Opus 5 (1M context), claude-opus-5[1m]

jonfroehlich and others added 2 commits August 12, 2026 15:36
)

Two admin-only additions around grants.

1. Three internal UW/Workday fields on Grant: uw_grant_id (the grant worktag,
   comma-separated when an award spans several), uw_award_number, and
   uw_award_name. All nullable. Shown in a "UW Internal Tracking" fieldset,
   with the worktag as a changelist column and all three searchable, since
   pasting a worktag from an email into the search box is the main use.

   These are internal administrative codes. GrantSerializer's field allowlist
   already excludes them; test_api.py now pins that, including a scan of the
   whole response body.

   Grant.grant_id is untouched: it is the *sponsor's* award ID (the NSF
   number), it is public, and it stays in the API. It gains a verbose_name of
   "Sponsor grant ID" so the two are distinguishable on the form.

2. A GrantTrackingLink model holding bookmarks to the official UW CSE and UW
   Award Portal trackers, rendered as a link bar atop the Grant changelist.
   These live in the database rather than in settings.py because the real URLs
   embed per-PI SharePoint sharing tokens and this repository is public; the
   URLField is max_length=1000 for the same reason (the SharePoint link blows
   past the 200-char default).

Permissions: Editors (PhD students, staff) now hold view_grant so they can look
up a worktag without pinging the PI, but nothing more. GrantAdmin hides the
funding amounts, proposal PDFs/raw files, the total-funding rollup, and the
tracking links from anyone who isn't a superuser, so the #1125 decision that
funding data stays with the superuser still holds. GrantTrackingLink is
superuser-only by the same mechanism as Award: absent from both group specs.

Note that Django's read-only rendering resolves labels through
admin.utils.label_for_field, which reads the model's verbose_name and ignores
ModelAdmin form labels — hence the model-level verbose_names, which is what
makes the labels correct on the read-only page Editors actually see.

Version 2.34.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
From QA on localhost:

- Reorder the changelist so the worktag sits right beside the title, which is
  what people open this page for: Title, UW Grant ID (worktag), Sponsor, Date.
  Superusers additionally get First Author (Last Name) and Funding Amount.
- Make the Editor column set an allowlist (EDITOR_LIST_DISPLAY) rather than the
  superuser list with sensitive columns filtered out, so a column added for
  superusers later cannot leak into the PhD-student view by omission.
- Prefetch authors only when the first-author column is actually rendered. The
  prefetch exists to keep that column from firing a query per row (#1346);
  Editors don't get the column, so they shouldn't pay for the join.
- Color the tracking-links/funding panel from the admin's own CSS custom
  properties instead of literal hex, so it follows the light/dark theme toggle.
  The hard-coded #f8f9fa/#ddd rendered as a light box with light text in dark
  mode (this predates the branch; the panel just grew enough to make it show).

Also verified against the real UW CSE SharePoint URL: 425 characters, which
passes URLValidator and needs the max_length=1000 field (Django's 200-char
default would reject it).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AdminBackupWarningTests writes a status fixture and then hits the real admin
views, which call get_backup_status() with the real clock. The fixture's
timestamps were hard-coded to 2026-08-07, so with BACKUP_STALE_AFTER_HOURS=36
a "healthy" fixture started reading as stale on 2026-08-08 and two tests have
failed on every run since:

  test_no_warning_when_backup_healthy
  test_data_health_panel_always_shown_even_when_healthy

Master's CI last passed on the #1443 merge itself and has been red ever since;
this surfaced while trying to merge #1449.

Timestamp the view suite's fixture relative to datetime.now(timezone.utc)
instead. The SimpleTestCase suites keep the fixed 2026-08-07 payload — they
pass an explicit now=NOW, so they were never time-dependent and pinning exact
values there is the point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jonfroehlich
jonfroehlich merged commit 8079879 into master Aug 12, 2026
3 checks passed
@jonfroehlich
jonfroehlich deleted the 1448-uw-grant-tracking branch August 12, 2026 23:19
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.

Add UW grant tracking: internal worktag/award fields + admin links to official UW/CSE trackers

1 participant