Skip to content

Commit 8ff1d73

Browse files
http://localhost:8000/users/me/
1 parent 030a146 commit 8ff1d73

4 files changed

Lines changed: 47 additions & 81 deletions

File tree

badges/display.py

Lines changed: 16 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,7 @@
2222
from django.utils import timezone
2323

2424
from badges.enums import BadgeLabel, TierRank, label_order, rank_order
25-
from badges.models import (
26-
RANK_LADDER_ORDER,
27-
Achievement,
28-
Badge,
29-
BadgeTier,
30-
UserBadge,
31-
)
25+
from badges.models import Achievement, UserBadge
3226
from badges.summary import user_badge_summary
3327
from core.constants import BadgeToken
3428

@@ -385,55 +379,29 @@ def _rank_key(user_badge):
385379
)
386380

387381

382+
# Stands in until the dialog is given the reader's own tallies.
383+
PLACEHOLDER_ACHIEVEMENT_COUNT = 1
384+
385+
388386
def achievement_dialog_rows():
389387
"""Every achievement type as a dialog row, Boost Day last.
390388
391-
Each row is iconed by the badge it feeds, so the dialog shows real artwork
392-
rather than a counter standing in for one.
389+
Each row carries a counter, which is what the design asks for: the tally is
390+
the point, the artwork being the same for every achievement type.
393391
394392
Ordered by name, ``Achievement`` being an admin-editable registry with no
395393
catalogue ordering of its own.
396394
"""
397-
achievements = Achievement.objects.prefetch_related(
398-
Prefetch(
399-
"badges",
400-
queryset=Badge.objects.prefetch_related(
401-
Prefetch(
402-
"tiers",
403-
queryset=BadgeTier.objects.filter(is_active=True).order_by(
404-
RANK_LADDER_ORDER
405-
),
406-
to_attr="active_tiers",
407-
)
408-
),
409-
to_attr="fed_badges",
410-
)
411-
)
412-
return [_achievement_row(achievement) for achievement in achievements] + [
413-
BOOST_DAY_ROW
395+
rows = [
396+
{
397+
"token": BadgeToken.ACHIEVEMENT_COUNT,
398+
"count": PLACEHOLDER_ACHIEVEMENT_COUNT,
399+
"name": achievement.name,
400+
"description": achievement.description,
401+
}
402+
for achievement in Achievement.objects.all()
414403
]
415-
416-
417-
def _achievement_row(achievement):
418-
"""One achievement, iconed by the entry tier of the badge it feeds.
419-
420-
An achievement with no badge, or one whose badge is briefly tierless while a
421-
retuned replacement is created, falls back to the counter. It carries 1
422-
rather than a real tally: the dialog names what each achievement is, and the
423-
reader's own counts are on their profile.
424-
"""
425-
row = {"name": achievement.name, "description": achievement.description}
426-
tier = next(
427-
(
428-
badge.active_tiers[0]
429-
for badge in achievement.fed_badges
430-
if badge.active_tiers
431-
),
432-
None,
433-
)
434-
if tier is None:
435-
return {**row, "token": BadgeToken.ACHIEVEMENT_COUNT, "count": 1}
436-
return {**row, "token": TIER_TOKENS[tier.rank]}
404+
return rows + [BOOST_DAY_ROW]
437405

438406

439407
def badge_dialog_rows():

badges/tests/test_recognition_dialogs.py

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@
99
from badges.display import (
1010
ACHIEVEMENT_BASED_ROW,
1111
BOOST_DAY_ROW,
12+
PLACEHOLDER_ACHIEVEMENT_COUNT,
1213
TENURE_ROW,
13-
TIER_TOKENS,
1414
achievement_dialog_rows,
1515
badge_dialog_rows,
1616
)
17-
from badges.enums import AchievementSlug, TierRank
18-
from badges.models import Achievement, BadgeTier
17+
from badges.models import Achievement
1918
from core.constants import BadgeToken
2019

2120
ACHIEVEMENTS = "v3/includes/_achievements_modal.html"
@@ -41,39 +40,23 @@ def test_achievement_rows_end_with_boost_day(catalogue):
4140
assert len(rows) == Achievement.objects.count() + 1
4241

4342

44-
def test_achievement_rows_are_iconed_by_the_badge_they_feed(catalogue):
45-
rows = {row["name"]: row for row in achievement_dialog_rows()}
46-
review = Achievement.objects.get(slug=AchievementSlug.LIBRARY_REVIEW)
43+
def test_achievement_rows_carry_a_counter(catalogue):
44+
"""Per Figma the achievement icon is a tally, not tier artwork."""
45+
rows = achievement_dialog_rows()[:-1]
4746

48-
assert rows[review.name]["token"] == TIER_TOKENS[TierRank.BRONZE]
49-
assert "count" not in rows[review.name]
47+
assert {row["token"] for row in rows} == {BadgeToken.ACHIEVEMENT_COUNT}
48+
assert {row["count"] for row in rows} == {PLACEHOLDER_ACHIEVEMENT_COUNT}
5049

5150

52-
def test_achievement_with_no_badge_falls_back_to_the_counter(db):
53-
achievement = Achievement.objects.create(slug="manual-only", name="Manual Only")
54-
55-
row, _boost_day = achievement_dialog_rows()
56-
57-
assert row["name"] == achievement.name
58-
assert row["token"] == BadgeToken.ACHIEVEMENT_COUNT
59-
assert row["count"] == 1
60-
61-
62-
def test_achievement_whose_badge_is_tierless_falls_back_to_the_counter(catalogue):
63-
"""A retuned badge is briefly tierless while its replacement is created."""
64-
achievement = Achievement.objects.get(slug=AchievementSlug.LIBRARY_REVIEW)
65-
BadgeTier.objects.filter(badge__achievement=achievement).update(is_active=False)
66-
67-
rows = {row["name"]: row for row in achievement_dialog_rows()}
51+
def test_single_digit_counts_render_padded(catalogue):
52+
"""A single digit is padded so the counter keeps one width."""
53+
out = render_to_string(ACHIEVEMENTS, {})
6854

69-
assert rows[achievement.name]["token"] == BadgeToken.ACHIEVEMENT_COUNT
55+
assert ">01<" in out
7056

7157

72-
def test_achievement_rows_cost_a_fixed_number_of_queries(
73-
catalogue, django_assert_num_queries
74-
):
75-
"""Prefetching keeps the row count from driving the query count."""
76-
with django_assert_num_queries(3):
58+
def test_achievement_rows_cost_one_query(catalogue, django_assert_num_queries):
59+
with django_assert_num_queries(1):
7760
achievement_dialog_rows()
7861

7962

core/templatetags/number_filters.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ def compact_number(value):
3333
@register.filter
3434
def k_count(value):
3535
"""
36-
Achievement-counter format per Figma spec: 1..999 shown as-is, 1000+ uses
37-
the "K" dimension — e.g. 1000 → "1K", 5500 → "5.5K", 10000 → "10K".
36+
Achievement-counter format per Figma spec: a single digit is padded to two
37+
so the counter keeps one width — e.g. 1 → "01", 9 → "09" — 10..999 are shown
38+
as-is, and 1000+ uses the "K" dimension: 1000 → "1K", 5500 → "5.5K".
3839
Non-numeric values are returned unchanged.
3940
"""
4041
if value is None:
@@ -44,7 +45,7 @@ def k_count(value):
4445
except (TypeError, ValueError):
4546
return value
4647
if n < 1000:
47-
return str(n)
48+
return f"{n:02d}" if 0 <= n < 10 else str(n)
4849
k = n / 1000
4950
formatted = f"{k:.1f}".rstrip("0").rstrip(".")
5051
return f"{formatted}K"

core/tests/test_templatetags.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from datetime import date, timedelta
33

44
from ..templatetags.date_filters import years_since
5+
from ..templatetags.number_filters import k_count
56
from ..templatetags.text_helpers import to_json
67

78

@@ -48,3 +49,16 @@ def test_to_json_escapes_html_special_chars():
4849
def test_to_json_prevents_script_injection():
4950
raw = to_json([{"value": "x", "label": "</script><script>alert(1)"}])
5051
assert "</script>" not in raw
52+
53+
54+
def test_k_count_pads_single_digits():
55+
"""The counter keeps one width, so 1..9 are shown as 01..09."""
56+
assert [k_count(n) for n in (0, 1, 9)] == ["00", "01", "09"]
57+
58+
59+
def test_k_count_leaves_wider_numbers_alone():
60+
assert [k_count(n) for n in (10, 99, 999)] == ["10", "99", "999"]
61+
62+
63+
def test_k_count_still_uses_the_k_dimension():
64+
assert [k_count(n) for n in (1000, 5500, 10000)] == ["1K", "5.5K", "10K"]

0 commit comments

Comments
 (0)