Skip to content

Story 2443: User Profile Integration – User Roles #2527

Open
julhoang wants to merge 7 commits into
developfrom
julia/implement-user-roles
Open

Story 2443: User Profile Integration – User Roles #2527
julhoang wants to merge 7 commits into
developfrom
julia/implement-user-roles

Conversation

@julhoang

@julhoang julhoang commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Issue: #2443

Summary & Context

Adds a real, user-controlled displayed profile role to the User model. A user features a library role they actually hold (Author / Maintainer / Contributor, optionally scoped to a library like "Boost.Beast Author"); staff assign internal C++ Alliance titles (e.g. "Board Member") in the admin. The role is resolved on read with no per-author N+1.

Changes

Profile role model & source of truth

  • Add a ProfileRole enum with two groups: library roles (Author / Maintainer / Contributor, user-selectable) and internal C++ Alliance titles (admin-only).
  • Add 4 fields to User:
    • displayed_profile_role + displayed_profile_role_library (FK) – the library role the user chose to feature, optionally scoped to one library.
    • internal_role – the staff-assigned C++ Alliance title.
    • resolved_profile_role – the auto-derived default (maintained by the recompute task, see below).
  • Make User.role the single source of truth, resolved by precedence: chosen role → internal title → auto-derived role.
  • Allow only one holder per singular title (CEO, CTO, CFO/COO, CMO, Chief of Staff) via a partial UniqueConstraint.
  • One migration adds the fields and the constraint.

Role selection (edit page)

  • The "Your Role" dropdown offers only the roles the user actually holds (generic labels first, then library-scoped). When they hold none it's disabled with a "Contribute to a library to unlock a role" hint.
  • Renders as a searchable combo (_field_combo.html) with a native <select> no-JS fallback.
  • The whole profile-edit card is now one <form> saved by the existing Save Changes button. The new save handler re-validates the choice against the user's own options, so they can never feature a role they don't hold.
  • The posts-feed sidebar card shows the user's real role.

Admin: assign C++ Alliance titles

  • Add EmailUserAdminForm: labels internal_role as "C++ Alliance title" and, when a singular title is already taken, blocks the save with an error naming the current holder (the DB constraint is the hard backstop).
  • Add a read-only "Derived library roles" panel showing the roles the user holds; library roles are never editable in the admin.

Role resolution & performance

  • .role reads the stored resolved_profile_role column directly, so a feed of N posts resolves every author's role with no per-author query.
  • A Celery task (recompute_displayed_profile_roles) refreshes that column after each library/commit import, plus a daily backstop. ‼️ It also clears a user's chosen role if a later import revokes their eligibility, so we never show a role they no longer hold. ‼️
  • Add select_related(...) for the scoped-role FK on the feed, homepage, and post-detail querysets.
  • Detailed contribution lists (edit dropdown, admin panel) are computed live per user. The ticket suggested a materialized view, but I chose a stored column + live queries instead:
    • Every read is a single user's slice (edit dropdown, admin) or one page of authors (feed) — all cheap via FK-indexed queries. A matview would precompute a full aggregation over ~153k commits just to serve those small slices.
    • The one place this ran per row — author role in the feed — is already handled by the resolved_profile_role column, so nothing latency-sensitive needs the matview.
    • A matview also carries standing cost: a refresh task + schedule, staleness between refreshes, and a second copy of the eligibility logic (the view's SQL alongside the live queries) that can drift.
    • At current scale (~600 eligibility rows over 153k commits) the live per-user queries are sub-millisecond, so the precompute buys nothing.

Cleanup

  • Delete users/profile_cards.py (user_profile_card); news cards use Entry.author.to_v3_profile_dict(), and to_v3_post_card_dict() drops its now-unused author_role argument.

Peer-Testing Guidelines

Setup

  1. Run docker compose exec web python manage.py migrate, then docker compose restart celery-worker celery-beat.
  2. Populate roles once: docker compose exec web python manage.py shell -c "from users.tasks import recompute_displayed_profile_roles as t; t()".

Find accounts that hold a role
3. Most accounts have no library contributions, so first find ones that do — run this SQL to list users with at least one eligible role:

WITH commit_counts AS (
    SELECT ca.user_id, lv.library_id, COUNT(*) AS commit_count
    FROM libraries_commitauthor ca
    JOIN libraries_commit c          ON c.author_id = ca.id
    JOIN libraries_libraryversion lv ON lv.id = c.library_version_id
    WHERE ca.user_id IS NOT NULL
    GROUP BY ca.user_id, lv.library_id
),
eligibility AS (
    SELECT la.user_id, la.library_id, 'Author' AS role
    FROM libraries_library_authors la
    UNION
    SELECT lm.user_id, lv.library_id, 'Maintainer' AS role
    FROM libraries_libraryversion_maintainers lm
    JOIN libraries_libraryversion lv ON lv.id = lm.libraryversion_id
    UNION
    SELECT cc.user_id, cc.library_id, 'Contributor' AS role
    FROM commit_counts cc
)
SELECT e.user_id, u.display_name, u.email,
       e.library_id, l.name AS library_name, e.role,
       COALESCE(cc.commit_count, 0) AS commit_count
FROM eligibility e
JOIN users_user u          ON u.id = e.user_id
JOIN libraries_library l   ON l.id = e.library_id
LEFT JOIN commit_counts cc ON cc.user_id = e.user_id AND cc.library_id = e.library_id
WHERE u.claimed IS TRUE
ORDER BY u.email, e.role, commit_count DESC;
  1. Spot-check a few of those users in the Users admin to confirm the roles they're eligible for match the query.
image

Exercise the role dropdown
5. Open http://localhost:8000/users/me/?edit=true. If your account has no library contributions, "Your Role" is disabled with the "Contribute to a library to unlock a role" hint. To see it populated (and pick/save a role), impersonate one of the eligible users from step 3 — ⭐️ there's a method to do this locally, reach out to me for details. ⭐️

Admin: C++ Alliance titles
6. In the Users admin, open your account and set a C++ Alliance title — the "Derived library roles" panel shows the eligible roles read-only. Then assign a singular title (e.g. CEO) to a second user — the save is blocked, naming the current holder.

Screenshots

Dropdown UI Updated User Profile Header Notes
Screenshot 2026-07-15 at 11 07 52 AM Screenshot 2026-07-15 at 11 13 37 AM Dropdown for a user with many roles
Screenshot 2026-07-15 at 11 09 03 AM Screenshot 2026-07-15 at 11 11 38 AM Disabled field for a user with no roles, the Profile Header role will be empty and not rendered.

Example of more roles filled out

  • Locally I've added the internal roles for some C++ Alliance folks.
  • Also notice the generic "Author" and the empty role – these are auto-determined
image

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 – ‼️ Currently No-JS is not yet supported, it'll be handled in Webpage Integration: Wire Up User Profile Edit Forms for Javascript-less Support #2509 ‼️
  • No console errors or warnings

Summary by CodeRabbit

  • New Features
    • Role selection is now driven by library eligibility, with tailored option labels and searchable role picking in the profile edit flow.
    • Administrators can assign internal titles and see role eligibility details, with validation to prevent duplicate internal title assignments.
  • Bug Fixes
    • Displayed profile roles refresh automatically as responsibilities change, with a daily recompute backstop.
    • Outdated or invalid role selections are cleared, and v3 author/profile rendering now consistently reflects the effective role.
  • Chores
    • Improved v3 page and community/news post performance by eager-loading role data.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d203f14b-8003-49e9-b85f-a8a2e397787e

📥 Commits

Reviewing files that changed from the base of the PR and between 9b21c3b and e212783.

📒 Files selected for processing (2)
  • users/tasks.py
  • users/tests/test_profile_role.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • users/tasks.py
  • users/tests/test_profile_role.py

📝 Walkthrough

Walkthrough

Introduces library-scoped and internal profile roles, dynamic profile-role selection, admin validation, periodic recomputation, and updated v3 author rendering with eager-loaded role relationships.

Changes

Displayed profile role system

Layer / File(s) Summary
Role model and persistence
users/models.py, users/migrations/...
Adds role categories, encoding helpers, persisted role fields, effective-role selection, eligibility ordering, and uniqueness constraints.
Role recomputation and triggers
users/tasks.py, libraries/tasks.py, config/celery.py
Recomputes derived roles, clears invalid selections, and schedules recomputation after library updates, commit updates, and daily at 9:00 AM.
Profile role selection and administration
users/forms.py, users/views.py, users/admin.py, templates/v3/user_profile_edit.html, static/css/...
Adds dynamic role choices, profile POST handling, admin validation and eligibility display, and profile-edit form layout.
V3 author and profile rendering
news/..., ak/homepage.py, core/views.py, templates/v3/posts_list.html
Uses computed author profile data in v3 cards, loads scoped role relationships, and replaces the hardcoded contributor role.
Role behavior tests
users/tests/test_profile_role.py
Covers eligibility, ordering, persistence, admin constraints, display output, and recomputation behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant CurrentUserProfileView
  participant V3UserProfileForm
  participant UserModel
  User->>CurrentUserProfileView: submit selected role
  CurrentUserProfileView->>V3UserProfileForm: load role options
  CurrentUserProfileView->>UserModel: decode and validate role selection
  UserModel-->>CurrentUserProfileView: persist displayed role and library
  CurrentUserProfileView-->>User: return updated profile context
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: julioest, herzog0, kattyode

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and clearly matches the main change: adding user profile roles.
Description check ✅ Passed The description covers the required issue, summary, links, changes, screenshots, and checklist, with only the risks section not explicitly filled.
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.
✨ 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/implement-user-roles

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 linked an issue Jul 15, 2026 that may be closed by this pull request
@julhoang julhoang changed the title Julia/implement user roles Story 2443: User Profile Integration – User Roles Jul 15, 2026
@julhoang julhoang marked this pull request as ready for review July 15, 2026 18:20

@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

🤖 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 `@users/tasks.py`:
- Around line 176-197: Update the stale-selection cleanup around stale_ids so it
cannot overwrite a concurrent legitimate selection: retain each stale user’s
exact displayed_profile_role and displayed_profile_role_library values, then
perform the clearing update inside the transaction only when both fields still
match those snapshot values. Keep the existing role-resolution updates unchanged
and ensure concurrent changes cause the conditional clear to affect no rows.

In `@users/tests/test_profile_role.py`:
- Around line 313-324: Strengthen the unauthorized-role rejection tests,
including test_post_rejects_role_for_unheld_library and the corresponding test
around the second reported range, by first assigning the user an eligible
displayed profile role for library before submitting the tampered role. After
the POST, assert the original role and library ID remain unchanged, proving the
request is rejected rather than ignored.
🪄 Autofix (Beta)

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: ff96fd6c-a85d-4211-8cda-0b492fe0003b

📥 Commits

Reviewing files that changed from the base of the PR and between 865a4ac and 9b21c3b.

📒 Files selected for processing (17)
  • ak/homepage.py
  • config/celery.py
  • core/views.py
  • libraries/tasks.py
  • news/models.py
  • news/views.py
  • static/css/v3/user-profile-page.css
  • templates/v3/posts_list.html
  • templates/v3/user_profile_edit.html
  • users/admin.py
  • users/forms.py
  • users/migrations/0022_user_displayed_profile_role_and_more.py
  • users/models.py
  • users/profile_cards.py
  • users/tasks.py
  • users/tests/test_profile_role.py
  • users/views.py
💤 Files with no reviewable changes (1)
  • users/profile_cards.py

Comment thread users/tasks.py Outdated
Comment thread users/tests/test_profile_role.py Outdated
@julhoang julhoang force-pushed the julia/implement-user-roles branch from e212783 to 1871c93 Compare July 16, 2026 22:31
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: Member Roles

1 participant