boostorg / boostorg/website-v2
Story #2558 :: Task: Show real earned badges on the v3 user profile (and honour hide_badges)- #2614
- Dominant language
- HTML
- Stars
- 18
- Forks
- 28
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 77
Description
Not a duplicate, Github thought the previous one (#2614) was closed since the branches changed order in the PR stack, so I'm reopening this.
# Issue: #2558
⚠️ Base branch is `teo/2541-release-pipeline-backfill`
## Summary & Context
The user-visible half. Real badges replace the placeholder gold medal everywhere a badge is
rendered: the profile page, the profile card, news author cards, the post list and the community
page.
`badges/display.py` is the whole presentation boundary. The v3 badge templates take a component
token and a label, never a model instance, so the rank-to-asset mapping lives there rather than on
the user model.
- **Figma link**:
- **Link to components/page:**
- - [http://localhost:8000/users/me/](http://localhost:8000/users/me/)
- - [http://localhost:8000/news/](http://localhost:8000/news/)
- - [http://localhost:8000/community/](http://localhost:8000/community/)
## Changes
- **`badges/display.py`**
- `held_badges` - the member's active badges, highest **rank** first, each rank once. Retiring a
tier keeps the badges already awarded against it, so a member who also qualifies under its
replacement holds the same rank twice; both rows are real history but only one is a badge to
show.
- `featured_badge` / `badge_cards` / `badge_card`.
- `TIER_TOKENS` - bronze/silver/gold/platinum/diamond to `badge-tier-1..5`, **diamond at the
top**. A test asserts the token numbers climb with the rank ladder so the pair cannot drift
apart again.
- `active_badges_prefetch(lookup)` - the prefetch every multi-card page needs.
- **`hide_badges` fails closed.** `held_badges` returns an empty list when the member has hidden
their badges; only the owner's own views pass `include_hidden`.
- **`User.featured_badge`** replaces the `badge_url` placeholder. It lives on the model only
because templates read it as an attribute; everything about selection and rendering is in
`badges.display`.
- **`users/profile_cards.py`**: emits `badge` / `badge_label`. It previously emitted `badge_url`
while `_user_profile.html` reads `author.badge`, so news author cards rendered **no badge at
all** even once real data existed. That is a defect on `develop`, fixed here because it is the
same feature.
- **Award dates** are `date` objects rendered through Django's `DATE_FORMAT`, taken through
`timezone.localtime` so an evening award is not dated to the next day west of UTC. There is a
test under `TIME_ZONE="America/New_York"` pinning it.
- **N+1 prefetches** at all four call sites that render more than one card: `ak/homepage.py`,
`core/views.py` (community), `news/views.py` (post detail), `libraries/mixins.py` and
`libraries/utils.py` (contributor lists). Each has a constant-query regression test. The
querysets that `select_related` the author ask for badges **through the path**
(`author__badges`), because `Prefetch("author", queryset=...)` is silently dropped when the FK
is already cached.
- **Fixes an inverted tier ladder on `develop`**: `core/views.py`'s v3 examples page labelled
tier-4 "Diamond" and tier-5 "Platinum" on both the badge and star sets. Platinum is tier 4,
diamond is tier 5. The same inversion in the `_badge_v3.html` header comment and the
profile-edit mock is corrected too.
## ‼️ Risks & Considerations ‼️
- **Every page that renders a user card is touched.** The risk is a missed prefetch rather than a
wrong badge; each call site has a `django_assert_num_queries` test, so a regression fails
loudly.
- `featured_badge` is a `cached_property`. A view that awards a badge and re-renders the same
user instance in one request would show the stale value. Nothing does that today.
- **Which badge is "featured" is not yet the member's choice** - it is simply the highest-ranked
one. Letting them pick is a separate ticket
Separate ticket.
- Badge **progress and locked states** on the profile are also a separate ticket
Separate ticket. This PR renders what a member has,
not what they could earn.
- `templates/v3/user_profile_page.html` switches from the `demo_badges` mock to `profile_badges`.
The `core/views.py` examples page keeps its `demo_*` fixtures, correctly.
## Screenshots
Before | After
-- | --
Placeholder gold medal on every card | The member's real highest badge, or nothing
Desktop Light Mode | Desktop Dark Mode | Mobile
-- | -- | --
## Peer-review testing steps
**Setup.** `just migrate`, `docker compose up`, and turn the **v3** flag on for everyone at
`/admin/waffle/flag/` - every template here is a v3 one. For step 8 you also need `DEBUG_TOOLBAR=True`
in `.env` and a restart.
1. **Award yourself a badge by hand.** `/admin/badges/userachievement/add/` - your user, achievement
**Library Authoring**, and a note (it is required for a manual grant). Save. Bronze is one grant, so
`/admin/badges/userbadge/` shows **Library Author / Bronze** immediately.
2. **The profile.** `/users/me/` - the placeholder gold medal is gone and Bronze is on the card. The
badges card lists it once, with the award date. Cross-check that date against *Awarded at* in the
admin: they must be the same day, which is the timezone fix.
3. **Only the highest badge is featured.** Add a second Library Authoring grant (duplicate manual
grants are intended, counts accumulate). Reload `/users/me/`: the card now shows **Silver**, while
the badges card lists Bronze *and* Silver. The card shows one badge, the list shows the ladder.
4. **Order and grandfathering.** Grant yourself two more (four in total) so you hold Gold. The badges
card lists Gold, Silver, Bronze in that order, highest rank first. Now raise Gold's threshold to 6 on
`/admin/badges/badge/`: the admin says it retired the old tier and created a replacement, and your
Gold **stays** on the profile even though you no longer meet the new number. That is the
grandfathering working as intended, not a stale render. (One rank showing twice is the case the
dedup in `held_badges` guards; it needs rows the admin will not create for you, so it is covered by
test rather than by hand.)
5. **Author cards elsewhere.** With a published post authored by you, open `/news/`, the post detail
page, `/community/` and the homepage. Your author card carries the same badge on all four. On the
base branch the news author card renders no badge at all even with real data, so this is a fix to
confirm, not just a check.
6. **Contributor lists.** Add yourself to **Authors** on a library's *latest* version at
`/admin/libraries/libraryversion/` (the contributor cards read the version, not the library), then
open `/library///`: those cards carry badges too.
7. **hide_badges fails closed.** Tick **Hide badges on your profile** at `/users/me/?edit=true`. Your
own profile still shows them, because the owner's view passes `include_hidden`. Then open
`/community/`, `/news/` and your post's detail page in a **logged-out** window: your card there has
no badge at all.
8. **No N+1.** With the debug toolbar open, note the SQL count on `/community/`, `/news/` and a post
detail page. Grant badges to several more members whose cards appear on those pages, reload, and the
count must be unchanged. A count that climbs with the number of cards is the regression these
prefetches exist to prevent.
9. **The ladder is the right way up.** `/v3/demo/components/` (staff only) - tier-4 is **Platinum** and
tier-5 is **Diamond**, on both the badge set and the star set. It was inverted on the base branch.
10. **Dark mode** on every page above, then **mobile width** on the profile and the post list.
11. **No JavaScript.** Disable it and reload `/users/me/` and `/community/`: badges are server-rendered
and must all still be there.
12. **Revocation is visible.** Revoke your **Gold** row from `/admin/badges/userbadge/` (the action asks
for a note). `/users/me/` falls back to Silver on the card, Gold leaves the badges list, and nothing
else moves.
## Self-review Checklist
- [ ] Tag at least one team member from each team to review this PR
- [ ] Link this PR to the related GitHub Project ticket
### Frontend
- [ ] UI implementation matches Figma design
- [ ] Tested in light and dark mode
- [ ] Responsive / mobile verified
- [x] No hardcoded colors, spacing or typography - the badge component is unchanged, only its data
- [x] Test without JavaScript - badges are server-rendered
- [x] No console errors or warnings
### Backend
- [x] Every multi-card call site has a constant-query test
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.