Skip to content

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

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

feat(2447): implement profile edit (visibility, acc. details, email prefs)#2517
ycanales wants to merge 7 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. Fixed.
  • 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

    • Updated the V3 profile editor into dedicated “Profile Details”, “Visibility”, and “Email Preferences” sections with separate Save actions.
    • Added optional country selection and public-visibility toggles for badges and activity, plus enhanced email preference handling.
    • Implemented section-aware saving with JSON responses for async submissions.
  • Bug Fixes

    • Prevent case-insensitive duplicate usernames.
    • Improved form field value initialization and error context; country “clear” now persists correctly.
  • Chores

    • Added/expanded automated test coverage for V3 profile edit and validation flows.

@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: 24 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: ce6f217a-faa0-4b9e-8a00-26b2f687e376

📥 Commits

Reviewing files that changed from the base of the PR and between 56afc75 and d347537.

📒 Files selected for processing (8)
  • templates/v3/includes/_button.html
  • templates/v3/includes/_field_text.html
  • templates/v3/user_profile_edit.html
  • users/forms.py
  • users/migrations/0023_user_country_user_hide_badges_and_more.py
  • users/models.py
  • users/tests/test_v3_profile_edit.py
  • users/views.py
📝 Walkthrough

Walkthrough

Adds country and profile-visibility fields, extends V3 form validation and email choices, restructures profile editing into independent section forms, adds section-specific request 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 curated 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, preserves Alpine field values, and adds AJAX fallback handling.
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, saved responses, 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: Return JSON or redirect
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: julioest, herzog0, julhoang

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: implementing v3 profile edit support for visibility, account details, and email preferences.
Description check ✅ Passed The description matches the template well, including issue, summary, links, changes, risks, screenshots, and checklist sections.
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.
✨ 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

@coderabbitai coderabbitai Bot Jul 7, 2026

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.

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.

@ycanales Would you mind confirming if this will be handled in #2521 ?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh pr view 2521 --repo boostorg/website-v2 --json title,body,state,url

Length of output: 484


@julhoang, I tried to look up #2521 for context but couldn't resolve it (GitHub CLI returned "Could not resolve to a PullRequest with the number of 2521"), so it may not exist yet, or the sandbox doesn't have visibility into it. I'll leave it to @ycanales to confirm whether that PR covers the DB-level uniqueness constraint for display_name. Happy to take another look once it's available or if there's more context to share.

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

@julhoang julhoang self-requested a review July 13, 2026 17:33

@julhoang julhoang 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.

Hi @ycanales ! This is great work! I just ran into 1 issue with saving the Override commit author email UI field. Would you mind double-checking this?

User.Profile.Field.not.saved.1.mp4

Also, per the Figma design, the button text should be updated to Changes saved instead of announcing via the toast message. Can we suppress the legacy toast message on this page?

Image

Comment thread templates/v3/user_profile_edit.html Outdated
<div class="card__cta_section">
{% include 'v3/includes/_button.html' with style='primary' type='submit' label='Save Changes' only %}
{% include 'v3/includes/_button.html' with style='primary' type='submit' name='v3_update_profile' label='Save changes' only %}
{% include 'v3/includes/_button.html' with style='secondary' url=profile_account_url label='Cancel' only %}

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.

Instead of redirecting user back to "My Profile", I think it'd make a greater UX if we allow users to remain on the Edit User page with just the temporary form data revert. To do that, I think we just need to replace url=profile_account_url with type='reset' here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch on the toast/"Changes Saves" button, here it is:
image

If you change again a value, the button changes back:
image

I addressed the "Cancel" behavior too, I wonder though, maybe the Figma is missing the "Cancel" in the other sections too, or could it be intentional?
image

@herzog0 herzog0 self-requested a review July 14, 2026 13:43

@herzog0 herzog0 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.

Looking good, just one nit, one requirement and one note: about the "Override commit author email address" feature, don't worry about that (cc @julhoang) as this will be addressed in a dedicated ticket. This feature shouldn't be implemented as this design reference states anyway.

Comment thread templates/v3/user_profile_edit.html Outdated
{% comment %} novalidate: this form still contains required fields (tagline, bio, links) with
no save handler yet; native constraint validation would silently block "Save changes" for
sections that are otherwise ready. Server-side validation is authoritative. {% endcomment %}
<form method="post" action="{% url 'profile-account' %}?edit=true" class="card__form" enctype="multipart/form-data" novalidate>

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.

Nit: Can we build the edit URL in the backend an hydrate it in the view, please?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed, thanks!

{% 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
Collaborator

Choose a reason for hiding this comment

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

Per the ticket, the user should also be able to select a blank/no country option to hide their flag if they don't wanna disclose that info anymore. Currently, there's no way to do that.

Image

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! Now when the user selects "No country" the select clears and is persisted when saving the form. Thanks Teo 👍
image

@julhoang julhoang Jul 16, 2026

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.

We actually do have a deselectable version of some dropdowns which were used on the Libraries page (I think I only added it to _field_combo_multi.html and _field_dropdown.html), I'm curious if we can use the same design here 🤔 That being said, I think the "None" option works great too!
cc @henryajisegiri

Screenshot 2026-07-16 at 2 20 36 PM

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
users/tests/test_v3_profile_edit.py (1)

147-164: 🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Non-submitted form sections are visually erased on validation failure.

Because the V3 page uses independent HTML <form> elements, submitting this section means request.POST only contains the details fields. However, when the backend encounters a validation error, it binds the entire V3UserProfileForm to that partial request.POST.

In Django, binding a form with missing fields causes those fields to evaluate to their empty defaults (e.g., empty strings for text fields, False for checkboxes), completely ignoring the initial data. Consequently, when the server returns a 200 OK HTML page to display the validation error, the user will see the fields in their other sections wiped out.

Please add an assertion to verify that cross-section fields (like tagline or hide_github) retain their initial values when validation fails, and implement a fix in the backend to populate missing fields before binding.

🐛 Proposed test enhancement
     with tp.login(user):
         response = tp.post(
             f"{tp.reverse('profile-account')}?edit=true",
             data={
                 "v3_update_details": "true",
                 "username": "taken",
                 "country": "US",
             },
         )
         assert response.status_code == 200
         assert "A user with that username already exists." in response.content.decode()
+        
+        # Ensure fields from other sections didn't get wiped out in the rendered HTML
+        html = response.content.decode()
+        assert user.tagline in html, "Fields from other sections were erased from the HTML response!"
     # user not changed

To fix the backend logic, you can merge the initial data into a mutable copy of request.POST inside post_v3 before passing it to V3UserProfileForm:

        # Ensure non-submitted fields don't render as empty/unchecked on validation failure
        post_data = request.POST.copy()
        for name, value in self.get_v3_edit_initial().items():
            if name not in section_fields and name not in post_data:
                # Add it as a string so bound_data can process it correctly
                if isinstance(value, bool):
                    if value:
                        post_data[name] = "on"
                elif value is not None:
                    post_data[name] = str(value)

        form = V3UserProfileForm(
            post_data,
            # ...
        )
🤖 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 147 - 164, Update the V3
validation flow in post_v3 so partial section submissions merge missing fields
from get_v3_edit_initial() into a mutable copy of request.POST before
constructing V3UserProfileForm; preserve submitted values, encode truthy
booleans as checked values, and omit false or null values. Extend
test_v3_update_details_duplicate_username_shows_inline_error to assert
cross-section fields such as tagline or hide_github retain their initial values
after the validation failure.
🧹 Nitpick comments (2)
users/forms.py (1)

233-235: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Resolve Ruff RUF005.

Prefer the unpacking form to satisfy the configured lint rule without changing behavior:

-        self.fields["country"].choices = [("", "No country")] + list(countries)
+        self.fields["country"].choices = [("", "No country"), *list(countries)]
🤖 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 233 - 235, Update the country choices assignment
in the form initializer to use iterable unpacking instead of list concatenation,
preserving the existing “No country” option followed by all countries and
satisfying Ruff RUF005.

Source: Linters/SAST tools

templates/v3/user_profile_edit.html (1)

305-342: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider registering the Alpine component via Alpine.data.

While assigning the initialization function to the global window object works perfectly, registering it via Alpine.data avoids polluting the global scope and aligns more closely with Alpine.js best practices for reusable components.

🛠️ Proposed refactor
-    window.createSectionForm = function (initialSaved) {
-      return {
+    document.addEventListener('alpine:init', () => {
+      Alpine.data('sectionForm', (initialSaved) => ({
         saved: initialSaved,
         _bypassFetch: false,
         async handleSubmit(event) {
           // ...
         },
-      }
-    }
+      }))
+    })

Then, you can update the HTML forms to use x-data="sectionForm(...)" instead of x-data="createSectionForm(...)".

🤖 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` around lines 305 - 342, Register the
section form component through Alpine.data using the existing createSectionForm
initialization logic instead of assigning it to window. Update all corresponding
form bindings from x-data="createSectionForm(...)" to x-data="sectionForm(...)",
preserving the current submit, success, and fallback behavior.
🤖 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.

Outside diff comments:
In `@users/tests/test_v3_profile_edit.py`:
- Around line 147-164: Update the V3 validation flow in post_v3 so partial
section submissions merge missing fields from get_v3_edit_initial() into a
mutable copy of request.POST before constructing V3UserProfileForm; preserve
submitted values, encode truthy booleans as checked values, and omit false or
null values. Extend test_v3_update_details_duplicate_username_shows_inline_error
to assert cross-section fields such as tagline or hide_github retain their
initial values after the validation failure.

---

Nitpick comments:
In `@templates/v3/user_profile_edit.html`:
- Around line 305-342: Register the section form component through Alpine.data
using the existing createSectionForm initialization logic instead of assigning
it to window. Update all corresponding form bindings from
x-data="createSectionForm(...)" to x-data="sectionForm(...)", preserving the
current submit, success, and fallback behavior.

In `@users/forms.py`:
- Around line 233-235: Update the country choices assignment in the form
initializer to use iterable unpacking instead of list concatenation, preserving
the existing “No country” option followed by all countries and satisfying Ruff
RUF005.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 18eb471d-acb1-4b00-a826-132a4e241f4d

📥 Commits

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

📒 Files selected for processing (5)
  • templates/v3/includes/_button.html
  • templates/v3/user_profile_edit.html
  • users/forms.py
  • users/tests/test_v3_profile_edit.py
  • users/views.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • templates/v3/includes/_button.html
  • users/views.py

…sibility

# Conflicts:
#	templates/v3/includes/_button.html
#	templates/v3/user_profile_edit.html
#	users/models.py
#	users/views.py
@ycanales

Copy link
Copy Markdown
Collaborator Author

Merged develop into my branch and solved conflicts while keeping changes from both sides.
Migration was changed from 0022 to 0023 and the dependency was updated.

@ycanales

Copy link
Copy Markdown
Collaborator Author

Addressed CodeRabbit suggestion on non-submitted form sections being visually erased on validation failure.

@ycanales ycanales requested review from herzog0 and julhoang July 16, 2026 17:36

@julhoang julhoang 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.

Woops we're now having an extra Save button in this section 😆, would you mind double-checking this?

Image

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

3 participants