Skip to content

Story #2504: Badges Dropdown Integration - #2592

Open
julhoang wants to merge 7 commits into
teo/2541-source-library-maintenancefrom
julia/badge-selection
Open

Story #2504: Badges Dropdown Integration#2592
julhoang wants to merge 7 commits into
teo/2541-source-library-maintenancefrom
julia/badge-selection

Conversation

@julhoang

@julhoang julhoang commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Issue: #2504

Stacked PR — based on teo/2541-source-library-maintenance, not develop. Review with that branch as the base.

Summary & Context

Replaces the static "Pick Badge" placeholder on the V3 profile-edit page with a working Display Badge picker, and gives the chosen badge somewhere to live: a new User.display_badge FK to badges.UserBadge.

The picker shows one row per badge category — the rung the member has reached for the categories they've started, the first rung for the ones they haven't — with unlocked categories leading and locked ones dimmed but legible, so the ladder reads as something to climb rather than a list of things you don't have.

This PR stores the selection; it does not yet render it on the public profile. Nothing outside the edit form reads display_badge — that consumer arrives with the badge profile-display work.

Behaviour Matrix

Each section below enumerate all of the potential cases that that a user can encounter while using the dropdown, and the test covering it. Hopefully this helps with testing / reviewing the key considerations! Each group is collapsed — open the one you are reviewing. 👇

Ordering and pre-selection — which rows show, in what order, and which one opens selected

Tests named below live in badges/tests/test_display.py and users/tests/test_v3_profile_edit.py unless a path is given — those are pre-existing badges coverage this PR relies on rather than adds.

Situation What the picker shows What is selected Covered by
No badges earned Every category, locked, at its first tier. Trigger reads "Unlock a badge to display on profile" Nothing selectable; saving stores NULL test_default_option_is_none_without_a_badge, test_locked_badges_read_in_catalogue_order, test_v3_update_profile_clears_display_badge_when_omitted
One badge earned, never chosen That row leads and is selectable Pre-selected — but derived. The column stays NULL until the member saves the Profile section test_v3_edit_page_preselects_the_best_badge_when_none_is_chosen (asserts both halves), test_earned_tier_is_selectable_and_names_its_user_badge
Badges at different ranks Unlocked rows first, highest rank first; locked rows after Highest rank wins across categories — a Gold Documenter beats a Bronze Library Author test_default_option_picks_the_highest_rank, test_unlocked_badges_lead_highest_rank_first
Two badges tied at the same rank Both lead; the tie breaks on BadgeLabel order Earlier category wins: Library Author → Version Author → Commits Master → Reviewer → Maintainer → Documenter → Regular → Publisher test_default_option_breaks_rank_ties_by_catalogue_order
Member has already chosen Their row is marked selected The stored choice wins; the default is only ever a fallback test_resolve_selection_keeps_a_choice_the_picker_still_offers, test_v3_update_profile_saves_display_badge
Locked categories Dimmed with a padlock on grey background Not selectable, and disabled in the no-JS <select> Selectability: test_unstarted_badge_shows_only_its_first_tier. Styling deliberately unasserted — it goes stale as the design shifts
Row text copies — what the second column reads in each state
Situation Row reads
Category under way, rungs above it The rung held plus the distance to the next — "Authored 1,613 commits, author 17,165 more commits to unlock the next badge"
Category at its top rung (Diamond) The rung held, no trailing clause — there is nothing above it
Category never started Its first tier as a target — "Author 1 library to unlock"
Threshold met, sync not yet run The earned phrase, but still locked — there is no UserBadge yet to store. Clears itself on the next recalculation
Category with no active tiers and none held The row is dropped entirely rather than shown as unreachable
New BadgeLabel added without copy Generic phrasing ("Earn 5 achievements to unlock") rather than a KeyError
Admin actions on a member's badges — revoke, reinstate, climb a rung
Situation Picker Stored selection Covered by
Admin manually revokes the displayed badge Row reverts to locked; the picker falls back to the next best badge The column keeps the stale pk, but it is ignored on render and rejected on save. Manual revocations survive recalculation, so it never returns on its own test_manually_revoked_tier_is_not_selectable, test_v3_edit_page_drops_a_revoked_display_badge, test_v3_update_profile_rejects_a_revoked_display_badge
Count drops below threshold (cascade revocation) Same as above Same — and because the pk is kept rather than cleared, the selection resumes by itself once the count recovers and the badge is re-awarded test_a_cascade_re_earn_returns_the_row_the_member_selected — pins the same-row re-earn the resumption depends on
Admin reinstates a manually revoked badge Row is selectable again The stored selection resumes with no action from the member badges/tests/test_admin.py::test_reinstate_action_clears_manual_revocation for the badge; resumption is the same same-row mechanism the cascade test pins
Member climbs a rung in the displayed category (Bronze → Silver) Only the Silver row is offered — a category shows one row, the rung reached. The Bronze UserBadge stays active but loses its row Falls forward onto Silver. Seeding the now-rowless Bronze pk would render a blank trigger test_resolve_selection_replaces_a_rung_the_member_climbed_past
UserBadge row hard-deleted Row gone on_delete=SET_NULL clears the column Not re-tested — stock Django FK behaviour, declared on the field
Member picks a badge, then a different category overtakes it in rank Their chosen row is still offered and stays selected Unchanged — rank only decides the default, never overrides a choice test_resolve_selection_keeps_a_choice_the_picker_still_offers
Threshold and tier changes — mostly the badges app's mechanics; the picker-side consequence is what we add

This section is largely guarded by tests that predate this PR — the ladder mechanics are the badges app's, not the picker's. The picker-side consequence is what we add.

Situation Result Covered by
Threshold raised via Replace tier The old tier is retired rather than edited, so members who met the old number keep their badge and the picker keeps offering that retired rung. Only new members face the new number test_services.py::test_replace_tier_keeps_the_old_tiers_holders, test_admin_badge_config.py::test_editing_a_threshold_keeps_existing_holders, test_services.py::test_grandfathering_threshold_change_does_not_revoke + test_retired_tier_still_held_keeps_its_row
Threshold lowered via Replace tier Members climb on the next recalculation; the picker then shows the higher rung and the stored selection follows it forward test_signals.py::test_lowering_a_threshold_awards_without_a_manual_rebuild, test_admin_badge_config.py::test_editing_a_threshold_awards_newly_qualifying_members; the follow-forward shares its mechanism with test_resolve_selection_replaces_a_rung_the_member_climbed_past
Threshold edited in place Everyone now below the number is cascade-revoked on the next sync and their row reverts to locked. This is exactly what replace_tier exists to avoid — worth knowing when debugging a badge that vanished Unreachable rather than untested: test_admin.py::test_tier_threshold_is_readonly_on_change and test_admin_badge_config.py::test_editing_a_threshold_retires_and_replaces_the_tier prove the admin forces Replace tier
A tier retired outright Badges already awarded survive. The picker keeps the retired rung for holders and skips it for everyone else; next_tier steps over the gap, so a Bronze holder is sent to Gold when Silver is retired test_services.py::test_deactivating_tier_preserves_existing_user_badges, test_retired_tier_still_held_keeps_its_row. The next_tier gap-stepping has no direct test — a pre-existing hole in badges.summary
A whole ladder shifted up, leaving a rung nobody can reach Recalculation never awards a rank at or below the highest one already held, so a retuning cannot quietly move a member down a rank. The "N more" clause counts to the next rank above them, not to the lowest unmet threshold test_admin_badge_config.py::test_shifting_the_whole_ladder_up_is_accepted, test_summary.py::test_summary_names_the_next_rank_up_after_every_threshold_shifts
Form, validation and fallbacks — what is rejected, and what happens without JS
Situation Result Covered by
JavaScript disabled The Alpine dropdown is replaced by a plain <select>, each option spelling the rank out ("Bronze Documenter — …"). Locked options are disabled Not asserted — just typical native component behaviour
Member posts another member's UserBadge pk Rejected — the field's queryset covers only their own active badges test_v3_update_profile_rejects_another_members_badge
Member posts a revoked pk Rejected by the same narrowing test_v3_update_profile_rejects_a_revoked_display_badge
Profile section saved with the field omitted Treated as show no badge; the column is cleared test_v3_update_profile_clears_display_badge_when_omitted
Member has Hide badges on The picker is unaffected — the choice still saves, it just has nowhere to show until the member turns the toggle off. The two controls sit in the same section, so the relationship is visible, but nothing on screen states it -
No badges configured in the database at all "No badges are configured yet" empty state (this case is very unlikely, but just to be safe) -

Notes and Decisions

  • Stored, not derived. Only the member's explicit choice is persisted. The rows, the default, and the validity check are all recomputed per render — which is what lets a revoked-then-reinstated badge come back without the member touching anything.
  • Pre-selection is a weak signal of intent. It shares a form section with the visibility toggles, so a member who saves that section for an unrelated reason persists the default badge without having deliberately picked it. Acceptable given the default is "your best badge", but worth knowing before anything treats the column as a deliberate choice.
  • BadgeLabel declaration order is load-bearing. Adding a category puts it wherever it is declared in the locked list; there is no separate ordering list to keep in step.
  • The picker owns no queries of its own. badge_options reads everything from user_badge_summary, including which rung the member has reached (AchievementRow.held). Anything the picker needs and the summary lacks should be added to the summary rather than fetched alongside it.

Changes

Storage

  • Add User.display_badge — nullable FK to badges.UserBadge with on_delete=SET_NULL, migration users/0027_user_display_badge.py. Pointing at the awarded tier rather than the Badge means the column carries category and rank.
  • Revocation is soft, so the column can outlive the badge's validity. The field's help_text says so, and every reader must check is_active rather than trusting the pointer.

Picker rows — badges/display.py (new)

  • badge_options(user) — one row per category. A started category shows the rung held, whose copy carries the distance to the next; an unstarted one shows its first tier. Unlocked lead by rank, then locked in catalogue order.
  • default_option(options) — the badge to open on when the member has never chosen: highest rank, catalogue order breaking ties.
  • resolve_selection(options, selected) — keeps a stored choice when the picker still offers it, otherwise falls back to the default.
  • Per-tier copy lives in PHRASES, keyed by BadgeLabel, with a generic fallback so a category added without copy renders rather than raising.
  • Every row is built from badges.summary.user_badge_summary — the arithmetic and the rung reached. Picking that rung by rank is subtle enough (retired tiers, manual revocations) that a second implementation would be a second set of bugs, and reusing it means the picker adds no queries.

Form and view

  • users/forms.pydisplay_badge is a ModelChoiceField whose queryset is narrowed in __init__ to the member's own active badges. That narrowing is the whole of the authorisation check.
  • users/views.pyget_v3_edit_initial drops a revoked selection before seeding, then resolves it against the picker's rows; get_v3_edit_context supplies badge_options. display_badge joins the v3_update_profile section and is written by _save_v3_visibility_section.
  • Removes the base branch's hardcoded badge_tiers context and the disabled select_title role field the placeholder used.

Template and styles

  • templates/v3/includes/_field_badge_select.html (new) — Alpine dropdown with a plain <select> fallback; exactly one of the two is ever enabled, so only one value submits. The trigger shows the badge icon; the no-JS options spell the rank out instead, an <option> having nowhere to put an icon.
  • static/css/v3/forms.css — badge-select block. The list is a grid with .badge-select__row as a subgrid so icon, name and detail line up across rows regardless of text length.
  • badges/enums.py — adds label_order() beside BadgeLabel, mirroring how rank_order() sits beside TierRank, and notes in the docstring that reordering those members reorders the picker.

Peer-Testing Guidelines

  1. Follow the setup in the Badge & Achievement PR so the achievements, badges and tiers exist.

Empty state

  1. In /admin/users/user/, open any member and click View badges and achievements in the Badges section. Confirm no badge is held — /admin/badges/userbadge/ lists only members who already hold one, so it can't answer this. Log in as that member.
  2. Open http://localhost:8000/users/me/?edit=true. The Display Badge trigger should read "Unlock a badge to display on profile". Every row is locked: dimmed text, grey background, a padlock, unclickable — with the badge icon still in full colour. Rows should be in catalogue order (Library Author → Version Author → Commits Master → Reviewer → Maintainer → Documenter → Regular → Publisher).

Earned state

  1. From /admin/badges/userbadge/, pick a member who holds an active badge and log in as them.
  2. Open http://localhost:8000/users/me/?edit=true. Each earned category shows a single row — the rung reached, not the whole ladder — and those rows lead the list, highest rank first, ahead of the locked ones. The best of them is pre-selected in the trigger, and its copy names the distance to the next rung ("…, author 17,165 more commits to unlock the next badge").
  3. Save the Profile section, reload, confirm the selection held. This save is what first writes the column — until then the pre-selection is derived per render, not stored. If the member holds two or more badges, pick a different one first and confirm that choice survives the reload rather than reverting to the best-ranked default.
  4. Grant enough achievements to climb a rung in the displayed category (the member's badge page has a prefilled grant link). Reload: the trigger should move up to the new rung, not go blank — the old rung's row disappears even though its UserBadge stays active.

Revocation

  1. In /admin/badges/userbadge/, select the displayed badge and run the Revoke action. Reload the edit page: that row is locked again and the picker falls back to the next best badge, or to the empty state if there is none.
  2. Run Reinstate on the same badge and confirm the member's original selection returns with no action from them.

Fallbacks

  1. Turn on Hide badges in the same section. The picker stays usable and the selection still saves — it simply has nowhere to appear while badges are hidden.
  2. Disable JavaScript and reload. The plain <select> should appear with ranks spelled out ("Bronze Documenter — …") and locked options greyed out; picking an unlocked one and saving should work.

Screenshots

Image Notes
Screenshot 2026-08-12 at 6 26 10 PM Dropdown for user with 1+ badges
Screenshot 2026-08-12 at 6 28 21 PM Dropdown for user with 0 bagde
Screenshot 2026-08-12 at 6 30 31 PM Mobile dropdown
Screenshot 2026-08-12 at 6 31 53 PM No-JS dropdown

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
  • Accessibility checked (keyboard navigation, etc.)
  • Ensure design tokens are used for colors, spacing, typography, etc. – No hardcoded values
  • Test without JavaScript (if applicable)
  • No console errors or warnings

Summary by CodeRabbit

  • New Features

    • Added a badge picker to the profile editor.
    • Users can select an earned badge tier to feature, with locked and unavailable tiers clearly identified.
    • Added badge details, progress information, icons, keyboard navigation, and accessible dropdown behavior.
    • Featured badges can be saved, changed, or cleared.
  • Bug Fixes

    • Revoked, inactive, and badges belonging to other users cannot be selected.
    • Badge choices now remain consistent after tier advancement or revocation.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f4df5328-4d76-467b-90cf-677558c454a6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds badge option generation and ordering, stores a user’s selected display badge, validates active user-owned badges, and replaces the profile’s static badge list with a progressively enhanced picker.

Changes

Profile badge selection

Layer / File(s) Summary
Badge display logic
badges/display.py, badges/enums.py, badges/tests/test_display.py
Adds badge ordering, tier phrases, option generation, default selection, stale-selection resolution, and coverage for badge states and ordering.
Badge persistence and profile flow
users/models.py, users/migrations/0027_user_display_badge.py, users/forms.py, users/views.py, users/tests/test_v3_profile_edit.py
Adds the nullable User.display_badge relation. The form and view restrict choices to active badges owned by the user and save, clear, or reject selections.
Badge picker UI
templates/v3/includes/_field_badge_select.html, templates/v3/user_profile_edit.html, static/css/v3/forms.css, static/css/v3/user-profile-page.css
Replaces the static badge list with a native-select fallback and Alpine.js dropdown. Adds picker styling and removes obsolete profile-specific styles.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProfileEditor
  participant BadgeDisplay
  participant BadgePicker
  participant ProfileForm
  participant UserModel
  ProfileEditor->>BadgeDisplay: build badge options
  BadgeDisplay-->>ProfileEditor: return ordered badge rows
  ProfileEditor->>BadgePicker: render options and initial selection
  BadgePicker->>ProfileForm: submit selected badge
  ProfileForm->>UserModel: validate and save display_badge
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: ycanales, jlchilders11

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: integrating the badge dropdown.
Description check ✅ Passed The description covers the issue, context, changes, behavior, testing guidance, screenshots, and checklist; the risks section is present but empty.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch julia/badge-selection

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@julhoang
julhoang changed the base branch from develop to teo/2541-source-library-maintenance August 7, 2026 21:18
@julhoang
julhoang force-pushed the julia/badge-selection branch from 03d8984 to 321522a Compare August 7, 2026 21:20
@julhoang julhoang linked an issue Aug 7, 2026 that may be closed by this pull request
4 tasks

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
users/migrations/0027_user_display_badge.py (1)

15-26: 🗄️ Data Integrity & Integration | 🔵 Trivial

Plan for lock time on the users table.

Adding this foreign key creates an index and a validated FK constraint on users_user. On a large table this blocks writes for the duration. If the table is large in production, consider applying the index and constraint concurrently in a follow-up operation, or run the migration in a low-traffic window.

The Ruff RUF012 hints on dependencies and operations are false positives for Django migrations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@users/migrations/0027_user_display_badge.py` around lines 15 - 26, The
AddField operation for User.display_badge may hold locks while creating and
validating the foreign-key index and constraint on a large users_user table.
Adjust the migration deployment strategy to apply these database changes
concurrently in a follow-up operation, or explicitly document and schedule this
migration for a low-traffic window, while preserving the existing field
semantics.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@templates/v3/includes/_field_badge_select.html`:
- Around line 85-97: Add a stable, matching listbox id to the dropdown panel and
set the trigger’s aria-controls to that id in the field badge select markup,
including the equivalent trigger block noted at the additional location.
Preserve the existing focus and ARIA behavior.
- Around line 30-33: Update the selected value initialization in the Alpine
x-data expression to safely encode the interpolated value for a JavaScript
single-quoted string, using escapejs or the existing data-attribute approach.
Preserve the current selected behavior and leave the integer UserBadge.pk
interpolations unchanged.

---

Nitpick comments:
In `@users/migrations/0027_user_display_badge.py`:
- Around line 15-26: The AddField operation for User.display_badge may hold
locks while creating and validating the foreign-key index and constraint on a
large users_user table. Adjust the migration deployment strategy to apply these
database changes concurrently in a follow-up operation, or explicitly document
and schedule this migration for a low-traffic window, while preserving the
existing field semantics.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dd1f6096-ef87-47fe-9eaa-c4402fa83ec3

📥 Commits

Reviewing files that changed from the base of the PR and between 32fb541 and 321522a.

📒 Files selected for processing (12)
  • badges/display.py
  • badges/enums.py
  • badges/tests/test_display.py
  • static/css/v3/forms.css
  • static/css/v3/user-profile-page.css
  • templates/v3/includes/_field_badge_select.html
  • templates/v3/user_profile_edit.html
  • users/forms.py
  • users/migrations/0027_user_display_badge.py
  • users/models.py
  • users/tests/test_v3_profile_edit.py
  • users/views.py
💤 Files with no reviewable changes (1)
  • static/css/v3/user-profile-page.css

Comment thread templates/v3/includes/_field_badge_select.html Outdated
Comment thread templates/v3/includes/_field_badge_select.html
@julhoang
julhoang force-pushed the julia/badge-selection branch from 321522a to 3f605d7 Compare August 11, 2026 21:08
@julhoang
julhoang marked this pull request as ready for review August 13, 2026 01:56
@herzog0
herzog0 force-pushed the julia/badge-selection branch from 49fa8fa to e80f0a7 Compare August 14, 2026 15:26
@herzog0
herzog0 force-pushed the julia/badge-selection branch from e80f0a7 to 0536e82 Compare August 14, 2026 19:57
@herzog0
herzog0 force-pushed the julia/badge-selection branch from 0536e82 to 6a7eb2d Compare August 14, 2026 20:07
@herzog0
herzog0 force-pushed the julia/badge-selection branch from 6a7eb2d to 46ae9ee Compare August 17, 2026 16:33
@julioest
julioest self-requested a review August 17, 2026 16:35
@herzog0
herzog0 force-pushed the julia/badge-selection branch from 46ae9ee to 16f91d7 Compare August 17, 2026 17:22
@herzog0
herzog0 force-pushed the julia/badge-selection branch from 16f91d7 to 8d47e22 Compare August 17, 2026 20:21
@herzog0
herzog0 force-pushed the julia/badge-selection branch 2 times, most recently from 75cd7a1 to c9e129f Compare August 18, 2026 14:28
@jlchilders11
jlchilders11 self-requested a review August 19, 2026 20:02

@julioest julioest left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NICE!

There is one issue that needs attention. The _detail function formats the "earned" phrase by using count=tier.threshold instead of the member's actual count. For example, with five commits, my row shows "Authored 1 commit, author 7 more commits to unlock the next badge". One plus seven equals eight, but the Silver badge requires twelve. Using the true count makes sense, as five plus seven totals twelve. Also, the pluralization comes from the threshold, which explains why it says "commit" for someone with five.

Current With the fix
row reading Authored 1 commit, author 7 more commits row reading Authored 5 commits, author 7 more commits

My Maintainer row currently states "Maintaining 2 libraries, maintain 3 more", which is entirely correct. This is because both that count and the threshold happen to be two. The test_display.py file contains no assertions for detail, so a test using convenient numbers would pass either way.

I tested a solution locally, and it functions. The change was roughly this:

earned = phrases.earned.format(
 count=intcomma(count), unit=_unit(phrases, count)
)

@herzog0
herzog0 self-requested a review August 20, 2026 13:16
@julhoang
julhoang requested a review from julioest August 20, 2026 22:04
@julhoang

Copy link
Copy Markdown
Collaborator Author

Thanks for the great catch @julioest ! I addressed it in a17b36c 🙌

@herzog0
herzog0 force-pushed the julia/badge-selection branch from a17b36c to ddef821 Compare August 21, 2026 14:48
@herzog0
herzog0 force-pushed the julia/badge-selection branch from ddef821 to 3bdb12b Compare August 24, 2026 13:40

@julioest julioest left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved!

@herzog0
herzog0 force-pushed the julia/badge-selection branch from 3bdb12b to dfa12c9 Compare August 24, 2026 19:22
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.

Webpage Integration: Edit Profile (Badges Dropdown)

3 participants