Skip to content

feat(2447): implement profile edit (visibility, acc. details, email prefs)#2517

Open
ycanales wants to merge 4 commits into
developfrom
cy/2447-profile-visibility
Open

feat(2447): implement profile edit (visibility, acc. details, email prefs)#2517
ycanales wants to merge 4 commits into
developfrom
cy/2447-profile-visibility

Conversation

@ycanales

@ycanales ycanales commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Issue: #2447

Summary & Context

Wires the v3 edit-profile page (shell landed in Story 2437) so three of its cards actually validate and persist: profile visibility toggles, account details (username, country), and email preferences. Adds four new User fields plus a per-section form pattern so each card saves independently.

Changes

  • Added a per-section form pattern: each card is its own <form> with a distinct named submit button (v3_update_profile, v3_update_details, v3_update_email_preferences); post_v3() dispatches on the button name and validates/saves only that section, so an unfinished card never blocks a completed one. Section wiring lives in the V3_EDIT_SECTIONS map in users/views.py.
  • Added a dispatch() override on CurrentUserProfileView to enforce login and route v3 POSTs into post()/post_v3() (the v3 mixin otherwise renders on every request and never reaches post), plus get_v3_edit_initial()/get_v3_edit_context() to seed the form from the current user.
  • Added four new fields to the User model (migration 0022): country (django-countries CountryField), hide_github_activity, hide_mailing_list_activity, and hide_badges (all BooleanField).
  • Update Details section saves Username (User.display_name, with case-insensitive uniqueness in clean_username) and Country (User.country), plus the existing indicate_last_login_method and is_commit_author_name_overridden toggles. Country was previously an empty plain dropdown and is now wired to the existing searchable combo (_field_combo.html) populated from django-countries.
  • Visibility section saves the three new hide toggles (hide_github / hide_ml / hide_ach).
  • Email Preferences section saves the two checkbox groups into Preferences.notifications (JSONField) via the existing property accessors, using a 4-item choice list (V3_EMAIL_PREFERENCE_CHOICES, no Poll to match Figma); unlisted news types a user already had enabled (e.g. Poll) are preserved on save.
  • Extended shared includes: _button.html accepts a name= so multiple submit buttons can coexist on one page, _field_text.html / _field_include.html surface server-side errors and seed values on re-render, and card.css adds .card__form so a <form> can wrap part of a card without breaking its flex-column layout.
  • Added users/tests/test_v3_profile_edit.py (11 tests) covering login-required GET/POST, current-value rendering, each section's save, blank-country clearing, duplicate-username inline error, email-prefs save, and the Poll-preservation edge case.

‼️ Risks & Considerations ‼️

Please list any potential risks or areas that need extra attention during review/testing

  • New migration 0022 adds four columns to the User table; needs to run on deploy.
  • email, the Commit email address card, Account connections, and the top of the Profile card (tagline, bio, links, avatar) are still UI-only scaffolding and do not persist yet. The override_commit_author_email toggle also has no backing field. These are intentionally out of scope for this PR.
  • Known visual bug: the Username/Email inputs in Update Details render around 22px tall instead of the 40px in Figma (the .field__control height is being overridden somewhere), so they look pill-shaped. Still being diagnosed.
  • Behavior is gated behind the v3 waffle flag.
  • Username validations: in this PR, username is only checked for an existing exact duplicate, more extensive rules will be implemented in this follow-up ticket Task: Username validations #2521

Screenshots

edit_01 localhost_8000_users_me__edit=true

Self-review Checklist

  • 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): separate ticket will deal with all segments of the page for no-JS functionality.
  • No console errors or warnings

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Refreshed the V3 profile editor into separate “Profile Details”, “Visibility”, and “Email Preferences” sections with dedicated submit actions.
    • Added country selection plus visibility toggles for badges and activity, and improved email preference handling.
  • Bug Fixes
    • Prevented case-insensitive duplicate display names.
    • Preserved entered values on validation errors and ensured partial section saves keep the rest of preferences intact.
    • Improved inline field error display and JS field value initialization.
  • Chores
    • Added automated coverage for V3 profile edit flows and validation.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ycanales, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 17d64604-e665-4c15-9b35-014c4642ac77

📥 Commits

Reviewing files that changed from the base of the PR and between 9378ef2 and e17725d.

📒 Files selected for processing (2)
  • users/forms.py
  • users/tests/test_v3_profile_edit.py
📝 Walkthrough

Walkthrough

Adds country and profile-visibility fields, extends profile form validation and choices, restructures V3 profile editing into independent section forms, adds section-specific view handling, updates field and card styling, and introduces comprehensive tests.

Changes

V3 profile edit multi-section update

Layer / File(s) Summary
User fields and form contracts
users/models.py, users/migrations/..., users/forms.py
Adds country and visibility fields, country choices, duplicate-username validation, revised labels, optional country input, and restricted email preference choices.
Sectioned V3 edit request handling
users/views.py
Adds authenticated V3 edit routing, section-aware validation, and persistence for visibility, details, and email preferences.
Independent profile edit forms
templates/v3/user_profile_edit.html, templates/v3/includes/_button.html, templates/v3/includes/_field_include.html, templates/v3/includes/_field_text.html
Splits editing into named forms, wires country and error values, supports named submit buttons, and initializes Alpine values from submitted data.
Card and field layout adjustments
static/css/v3/card.css, static/css/v3/forms.css
Adds .card__form layout styling and removes the fixed label-row height.
V3 profile edit behavior tests
users/tests/test_v3_profile_edit.py
Tests authentication, initial values, section updates, country clearing, duplicate usernames, and email preference preservation.

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

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant CurrentUserProfileView
  participant V3UserProfileForm
  participant User

  Browser->>CurrentUserProfileView: POST profile-account?edit=true with section button
  CurrentUserProfileView->>CurrentUserProfileView: dispatch() checks authentication
  CurrentUserProfileView->>CurrentUserProfileView: post_v3() selects submitted section
  CurrentUserProfileView->>V3UserProfileForm: Validate section-owned fields
  V3UserProfileForm-->>CurrentUserProfileView: Validation result
  CurrentUserProfileView->>User: Save selected section when valid
  CurrentUserProfileView-->>Browser: Re-render errors or redirect with success
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: jlchilders11, julioest, herzog0

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 summarizes the main change: implementing the v3 profile edit flow for visibility, account details, and email preferences.
Description check ✅ Passed The description matches the repository template well, including issue, summary, changes, risks, screenshots, and checklist items.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cy/2447-profile-visibility

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.

@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: 3

🧹 Nitpick comments (2)
users/tests/test_v3_profile_edit.py (1)

68-84: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Add a True→False transition case for a hide toggle.

This test only exercises turning toggles on from their default (False) state; hide_badges is False is asserted but never actually toggled off from a pre-existing True. Consider seeding user.hide_badges = True before the POST (with hide_ach omitted) to confirm the section correctly unsets it.

🤖 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/tests/test_v3_profile_edit.py` around lines 68 - 84, The v3 profile
edit test only covers toggles being turned on from their default state, so it
misses the True-to-False path for a hide toggle. Update the test in
test_v3_update_profile_saves_visibility_toggles by seeding the user’s
hide_badges to True before the POST, then omit the corresponding toggle field in
the form data so the profile update flow can be verified to clear it. Keep the
existing assertions on user.refresh_from_db() and add/adjust the final assertion
to confirm hide_badges is unset after the update.
users/views.py (1)

536-557: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Mutable class attribute flagged by Ruff (RUF012).

V3_EDIT_SECTIONS is a plain dict assigned as a class attribute; Ruff's RUF012 recommends annotating it as typing.ClassVar[dict[...]] (or using a different construct) to make the shared, immutable-in-practice nature explicit and silence the linter.

♻️ Suggested annotation
+from typing import ClassVar
+
-    V3_EDIT_SECTIONS = {
+    V3_EDIT_SECTIONS: ClassVar[dict] = {
🤖 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/views.py` around lines 536 - 557, V3_EDIT_SECTIONS is a mutable
class-level dict and Ruff flags it under RUF012; update the declaration to make
its shared, read-only intent explicit by annotating it with typing.ClassVar (or
moving it to an immutable construct). Keep the fix centered on the
V3_EDIT_SECTIONS constant in the view class so the linter recognizes it as a
class attribute rather than an instance field.

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/user_profile_edit.html`:
- Line 180: The Country field in the user profile edit template is passing a
separate placeholder even though country_options already contains the empty
“Select” option, causing duplicate “Select” entries in the no-JS select. Update
the include for _field_combo.html in user_profile_edit.html to rely on
country_options alone and remove the placeholder argument, keeping the
selected/error wiring unchanged.

In `@users/forms.py`:
- Around line 255-265: The `clean_username` check in `users.forms` only
validates uniqueness at the form layer, so concurrent writes can still create
duplicate `User.display_name` values. Add a database-backed case-insensitive
uniqueness guarantee on `users.User` using a `UniqueConstraint` with
`Lower("display_name")`, and include the necessary migration/data cleanup to
resolve any existing duplicates before applying it. Also update the save path
for `User` so `IntegrityError` from the database is caught and surfaced as the
same “already taken” validation error, keeping the enforcement consistent in
both `clean_username` and the model save flow.

In `@users/tests/test_v3_profile_edit.py`:
- Around line 86-105: Add coverage for the omitted-checkbox path in the v3
profile edit tests: the current test for v3_update_details only checks the
checked state, but _save_v3_details_section reads booleans directly from
form.cleaned_data so absent checkbox keys should reset to False. Add a test
around test_v3_update_details_saves_account_fields (or a sibling test) that
starts with is_commit_author_name_overridden and indicate_last_login_method
already True, submits the profile-account form via tp.post without those two
keys, and asserts both fields become False after refresh_from_db.

---

Nitpick comments:
In `@users/tests/test_v3_profile_edit.py`:
- Around line 68-84: The v3 profile edit test only covers toggles being turned
on from their default state, so it misses the True-to-False path for a hide
toggle. Update the test in test_v3_update_profile_saves_visibility_toggles by
seeding the user’s hide_badges to True before the POST, then omit the
corresponding toggle field in the form data so the profile update flow can be
verified to clear it. Keep the existing assertions on user.refresh_from_db() and
add/adjust the final assertion to confirm hide_badges is unset after the update.

In `@users/views.py`:
- Around line 536-557: V3_EDIT_SECTIONS is a mutable class-level dict and Ruff
flags it under RUF012; update the declaration to make its shared, read-only
intent explicit by annotating it with typing.ClassVar (or moving it to an
immutable construct). Keep the fix centered on the V3_EDIT_SECTIONS constant in
the view class so the linter recognizes it as a class attribute rather than an
instance field.
🪄 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: 1dce707a-6b90-405a-a972-3619c7aeeb50

📥 Commits

Reviewing files that changed from the base of the PR and between c90bdd1 and 9844991.

📒 Files selected for processing (10)
  • static/css/v3/card.css
  • templates/v3/includes/_button.html
  • templates/v3/includes/_field_include.html
  • templates/v3/includes/_field_text.html
  • templates/v3/user_profile_edit.html
  • users/forms.py
  • users/migrations/0022_user_country_user_hide_badges_and_more.py
  • users/models.py
  • users/tests/test_v3_profile_edit.py
  • users/views.py

{% include 'v3/includes/_field_include.html' with bound_field=user_profile_form.username %}
{% include 'v3/includes/_field_include.html' with bound_field=user_profile_form.email %}
{% include 'v3/includes/_field_include.html' with bound_field=user_profile_form.country %}
{% include 'v3/includes/_field_combo.html' with name=user_profile_form.country.name label='Country' options=country_options selected=user_profile_form.country.value placeholder='Select' error=user_profile_form.country.errors.0 %}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd _field_combo.html
cat templates/v3/includes/_field_combo.html 2>/dev/null

Repository: boostorg/website-v2

Length of output: 7378


Avoid passing a separate placeholder here

country_options already includes ("", "Select"), so placeholder='Select' makes the no-JS <select> render two identical “Select” entries. Drop one of them.

🤖 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 `@templates/v3/user_profile_edit.html` at line 180, The Country field in the
user profile edit template is passing a separate placeholder even though
country_options already contains the empty “Select” option, causing duplicate
“Select” entries in the no-JS select. Update the include for _field_combo.html
in user_profile_edit.html to rely on country_options alone and remove the
placeholder argument, keeping the selected/error wiring unchanged.

Comment thread users/forms.py
Comment on lines +255 to +265
def clean_username(self):
username = self.cleaned_data["username"]
if not username:
return username
existing = User.objects.filter(display_name__iexact=username)
if self._user is not None:
existing = existing.exclude(pk=self._user.pk)
if existing.exists():
raise forms.ValidationError("This username is already taken")
return username

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files users/forms.py users/models.py users/migrations || true

printf '\n== users/forms.py (around clean_username) ==\n'
nl -ba users/forms.py | sed -n '230,290p'

printf '\n== users/models.py (relevant parts) ==\n'
nl -ba users/models.py | sed -n '1,260p'

printf '\n== search for display_name constraints / uniqueness / integrity handling ==\n'
rg -n "display_name|UniqueConstraint|unique=True|IntegrityError|Lower\(" users -g '!**/__pycache__/**'

printf '\n== migrations mentioning display_name ==\n'
rg -n "display_name|UniqueConstraint|unique=True|Lower\(" users/migrations -g '!**/__pycache__/**'

Repository: boostorg/website-v2

Length of output: 1353


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== users/forms.py (around clean_username) ==\n'
sed -n '240,285p' users/forms.py | cat -n

printf '\n== users/models.py (top + User model area) ==\n'
sed -n '1,260p' users/models.py | cat -n

printf '\n== users/migrations/0009_user_display_name.py ==\n'
sed -n '1,220p' users/migrations/0009_user_display_name.py | cat -n

printf '\n== users/migrations/0017_populate_users_display_name.py ==\n'
sed -n '1,220p' users/migrations/0017_populate_users_display_name.py | cat -n

printf '\n== search for IntegrityError / unique constraints / display_name ==\n'
grep -RIn --exclude-dir='__pycache__' -E 'display_name|UniqueConstraint|unique=True|IntegrityError|Lower\(' users | head -n 200

Repository: boostorg/website-v2

Length of output: 19906


users/forms.py:255-265 — Add a DB-level uniqueness constraint for User.display_name
clean_username() only enforces this in the form, so concurrent saves can still create duplicate display names. Add a case-insensitive UniqueConstraint(Lower("display_name")) on users.User with a migration/data cleanup for existing duplicates, and handle IntegrityError on save so the “already taken” error is enforced by the database too.

🤖 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/forms.py` around lines 255 - 265, The `clean_username` check in
`users.forms` only validates uniqueness at the form layer, so concurrent writes
can still create duplicate `User.display_name` values. Add a database-backed
case-insensitive uniqueness guarantee on `users.User` using a `UniqueConstraint`
with `Lower("display_name")`, and include the necessary migration/data cleanup
to resolve any existing duplicates before applying it. Also update the save path
for `User` so `IntegrityError` from the database is caught and surfaced as the
same “already taken” validation error, keeping the enforcement consistent in
both `clean_username` and the model save flow.

Comment thread users/tests/test_v3_profile_edit.py
@ycanales

ycanales commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

On removing the height: 100% on .field__label-row in static/css/v3/forms.css:

In the Update Details card, the field sits in an unbroken chain of definite-height containers: the grid stretches .user-profile__col to the row height, and from there card → .user-profile__xl-space-col → .field are all column-flex layouts. In that context the label row's height: 100% actually resolves — against the whole .field height — so the label row's flex base size ballooned to ~70px. The field then had more base content than room, and flex-shrink compressed the .field__control from its specified 40px down to its ~20px min-content floor (this is why the height looked "ignored": flex shrink overrides even an inline height: 40px !important).

Tagline and the other inputs never broke because they sit inside plain block divs, which make the parent height indefinite; there height: 100% silently fell back to auto.

Before:
image

After:
image

I double-checked other views to confirm heights were not affected:

  • /accounts/login - email field
  • /accounts/signup/ - email and username fields
  • /v3/news/add/ - title and link fields
  • /libraries/ - search field
  • /news/ - search field

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 User Profile - Visibility Toggles, Email Prefs & Update Details

1 participant