Skip to content

Story 2449: Webpage Integration: Edit User Profile - Profile Links#2514

Merged
herzog0 merged 2 commits into
developfrom
javiercoronarv/edit-user-profile-links
Jul 16, 2026
Merged

Story 2449: Webpage Integration: Edit User Profile - Profile Links#2514
herzog0 merged 2 commits into
developfrom
javiercoronarv/edit-user-profile-links

Conversation

@javiercoronadonarvaez

@javiercoronadonarvaez javiercoronadonarvaez commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Issue: #2449

Summary & Context

Adds the Profile Links section to the edit profile page (GitHub, Website, Email, Slack), with inline validation and persistence. Links are batched into a single Save Changes request alongside the rest of the Profile section, persisted to the User record, and pre-filled from the saved data on load.

Changes

  • Add a profile_links (JSONField, keyed by link type) field to the User model via migration 0022.
  • Expose profile_links on CurrentUserSerializer (writable) and pre-fill the edit form from user.profile_links (replacing the previous hardcoded placeholder).
  • Edit profile page: the four locked link rows (GitHub, Website, Email, Slack) now persist on Save Changes via PATCH /api/v1/users/me/, with a Saved / error indicator on the button.
  • Inline validation: GitHub and Website require an HTTPS URL, otherwise show Please add a secure link. The error clears as the user edits the field. Email and Slack accept free-form input.
  • Slack takes an identifier (e.g. @vinniefalco); the frontend strips a leading @ and stores the canonical team URL https://cpplang.slack.com/team/[identifier]. On prefill the URL is reversed back to the bare identifier so the field matches the design.
  • Add an optional alpine_click to the shared _button.html include.

‼️ Risks & Considerations ‼️

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

Kept PATCH /api/v1/users/me/ instead of the ticket's /api/user/profile endpoint (same reasoning as the sibling bio/tagline PR #2502): PATCH is the correct partial-update verb, the endpoint already exists and is tested (CurrentUserAPIView + CurrentUserSerializer), and it keeps the codebase's /api/v1/ versioning convention.

Links are stored in a single profile_links JSONField keyed by type, rather than four separate columns. This maps 1:1 to the existing V3UserProfileForm(user_links={...}) dict interface (clean prefill), batches all four links into one PATCH, and lives in its own field so it never clobbers the shared data JSONField.

Slack is persisted as the full constructed URL, not the raw identifier. The SLACK_TEAM_URL_PREFIX constant in users/forms.py is the single source of truth. The JS construction (user_profile_edit.html) and the Python prefill reversal are kept in sync against it. If the prefix ever changes, update both.

Validation is client-side and inline only — no server-side URL validation. Per the ticket, moderation/flagging of suspicious URLs and reachability/safety checks are explicitly out of scope.

Migration numbering. This PR adds 0022_user_profile_links on top of 0021; PR #2502 also adds a 0022_*. They don't collide on disk, but whichever merges second may need a Django --merge migration (or a renumber to 0023).

Out of scope (intentionally deferred): rendering the saved links on the public profile (social_media_links still points to #), the design's "+ Add Another" affordance (the four types are fixed/locked per the acceptance criteria), and batching avatar/role/badges into the same save request.

Screenshots

LocalStorage and DB Persistence

ProfileLinksPersistence.mov

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

Summary by CodeRabbit

  • New Features
    • Added editable “Profile Links” (GitHub, website, email, Slack) with save status and inline validation.
  • Bug Fixes
    • “Edit Profile” now links correctly to the edit view.
    • Slack link editing/display now shows the team identifier (not the full URL).
  • Other Changes
    • Increased supported link length (up to 200 characters) and improved form targeting for link fields.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds profile_links storage and serializer support, normalizes Slack link display, updates profile edit/header links, and converts profile link editing to an Alpine.js save flow. It also adds optional Alpine click support to the shared button include.

Changes

Profile links feature

Layer / File(s) Summary
Profile links field and serialization
users/migrations/0022_user_profile_links.py, users/models.py, users/serializers.py, users/tests/test_serializers.py
Adds profile_links to the user schema, exposes it on CurrentUserSerializer, validates allowed keys and string lengths, and adds serializer tests.
Slack display normalization
users/forms.py, users/tests/test_forms.py
Adds Slack prefix stripping for edit-form display and tests the display helper for Slack and non-Slack values.
Profile edit context and header link
core/context_processors.py, users/views.py, templates/v3/includes/header/_header_avatar.html
Adds a shared edit-profile URL helper, uses stored profile links in edit mode, and switches the header avatar edit link to the shared helper.
Button Alpine click support
templates/v3/includes/_button.html
Documents and conditionally renders an optional Alpine click binding on the shared button include.
Profile edit Alpine save flow
templates/v3/user_profile_edit.html, templates/v3/includes/_field_text.html
Adds Alpine state, per-link data attributes, inline validation/error handling, and a PATCH-based save flow for profile links.

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

Possibly related issues

Possibly related PRs

  • boostorg/website-v2#2478 — Shares the v3 profile edit UI area and precedes the Alpine-driven profile link editing flow.

Suggested reviewers: jlchilders11, ycanales, herzog0

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: adding Profile Links to the edit profile page.
Description check ✅ Passed The description matches the template with issue, summary, changes, risks, screenshots, and checklist sections filled in.
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 javiercoronarv/edit-user-profile-links

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.

@javiercoronadonarvaez javiercoronadonarvaez linked an issue Jul 1, 2026 that may be closed by this pull request
@javiercoronadonarvaez
javiercoronadonarvaez marked this pull request as ready for review July 1, 2026 20:00

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

Caution

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

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

29-45: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

No server-side validation for profile_links — writable field accepts arbitrary/malformed JSON.

profile_links is now writable via PATCH /api/v1/users/me/ with zero validation: no allowlist of keys (github/website/email/slack), no enforcement of HTTPS-only URLs for github/website (a stated PR requirement, currently only enforced client-side in Alpine.js), and no type checking on values. A client can persist {"slack": null} or {"anything": {...}}, which crashes V3UserProfileForm._display_link_value in users/forms.py (unconditional value.startswith(...)) on the very next edit-profile page load, and unvalidated URL schemes are a latent XSS/open-redirect risk if these links are later rendered as href on public profiles.

🛡️ Suggested fix: add serializer-level validation
+    ALLOWED_LINK_TYPES = {"github", "website", "email", "slack"}
+
+    def validate_profile_links(self, value):
+        if not isinstance(value, dict):
+            raise serializers.ValidationError("profile_links must be an object.")
+        unknown = set(value) - self.ALLOWED_LINK_TYPES
+        if unknown:
+            raise serializers.ValidationError(f"Unsupported link types: {sorted(unknown)}")
+        for key in ("github", "website"):
+            val = value.get(key)
+            if val and not (isinstance(val, str) and val.startswith("https://")):
+                raise serializers.ValidationError({key: "Must be a secure https:// URL."})
+        for key, val in value.items():
+            if val is not None and not isinstance(val, str):
+                raise serializers.ValidationError({key: "Must be a string."})
+        return value
+
     class Meta:
         model = User
🤖 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/serializers.py` around lines 29 - 45, `UserSerializer` currently allows
`profile_links` to be written with no server-side checks, so add
serializer-level validation for that field. In the serializer (and any dedicated
`validate_profile_links`/update logic), restrict keys to the approved set
(`github`, `website`, `email`, `slack`), require string values where
appropriate, and enforce HTTPS-only URLs for `github` and `website` before
saving. Also reject nulls and unexpected nested objects so
`V3UserProfileForm._display_link_value` and later profile rendering only receive
well-formed data.
🤖 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 145: The Save Changes button in user_profile_edit.html now calls save()
directly, but that method only PATCHes profile_links and skips the rest of the
profile form state. Update the save flow so the button submits the full profile
payload, or extend save() to include all edited profile fields along with links;
use the existing save(), profile_links, and the form’s profile data bindings as
the entry points.
- Around line 445-454: The Slack normalization in collectLinks() is out of sync
with the persisted contract: it currently converts the input into a full team
URL, while users/models.py documents Slack as a bare identifier. Update the
collectLinks() Slack branch to preserve the stripped identifier if that is the
intended storage format, or otherwise update the model/help text and any
display/prefill builders to consistently use the URL format. Make sure the
normalization also handles pasted full Slack URLs without creating double
prefixes.
- Around line 436-438: The profile link validator currently accepts any value
that merely starts with https://, which still allows invalid or unusable URLs to
pass. Update the github/website validation branch in the user_profile_edit.html
form logic to parse the value as a real URL and validate the protocol/host,
instead of relying on a prefix check, so only usable secure links are accepted.

In `@users/forms.py`:
- Around line 248-256: Guard `_display_link_value` against non-string inputs
before calling `startswith`, since `value` can be `None` or another JSON type
and crash the edit-profile render. Update the static method
`_display_link_value` in `users/forms.py` to first verify `value` is a string,
then apply the Slack URL trimming logic only for string values; otherwise return
the original value unchanged. Keep the existing `V3ProfileLinkChoices.SLACK` and
`SLACK_TEAM_URL_PREFIX` handling intact.

In `@users/models.py`:
- Around line 264-271: The help text on profile_links is describing Slack
storage backwards. Update the JSONField help_text in the profile_links
definition so it matches the actual behavior in users/forms.py and the template
logic: Slack links are stored as the full canonical team URL and only converted
to a display-friendly value by _display_link_value/SLACK_TEAM_URL_PREFIX and
collectLinks(). Keep the wording aligned with that storage/display flow and
reference profile_links as the field to update.

---

Outside diff comments:
In `@users/serializers.py`:
- Around line 29-45: `UserSerializer` currently allows `profile_links` to be
written with no server-side checks, so add serializer-level validation for that
field. In the serializer (and any dedicated `validate_profile_links`/update
logic), restrict keys to the approved set (`github`, `website`, `email`,
`slack`), require string values where appropriate, and enforce HTTPS-only URLs
for `github` and `website` before saving. Also reject nulls and unexpected
nested objects so `V3UserProfileForm._display_link_value` and later profile
rendering only receive well-formed data.
🪄 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: 4d624b59-40a3-47e2-adc1-c2a459eb4ba6

📥 Commits

Reviewing files that changed from the base of the PR and between d004c67 and 8fb827c.

📒 Files selected for processing (8)
  • templates/v3/includes/_button.html
  • templates/v3/includes/header/_header_avatar.html
  • templates/v3/user_profile_edit.html
  • users/forms.py
  • users/migrations/0022_user_profile_links.py
  • users/models.py
  • users/serializers.py
  • users/views.py

{% comment %} Persists the Profile links to the current user via PATCH /api/v1/users/me/. {% endcomment %}
<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='button' label='Save Changes' alpine_disabled="saving" alpine_click="save()" only %}

@coderabbitai coderabbitai Bot Jul 1, 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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve saving for the rest of the profile form.

The new button bypasses the previous submit flow, but save() PATCHes only profile_links. Edits to the other profile fields in this card will be dropped instead of being saved together with the links.

Also applies to: 475-481

🤖 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 145, The Save Changes button in
user_profile_edit.html now calls save() directly, but that method only PATCHes
profile_links and skips the rest of the profile form state. Update the save flow
so the button submits the full profile payload, or extend save() to include all
edited profile fields along with links; use the existing save(), profile_links,
and the form’s profile data bindings as the entry points.

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.

Intentional for this PR. On the V3 edit page the "Save Changes" button never persisted the other Profile fields. So save() isn't dropping anything that previously worked. Instead, it adds the first real persistence (links).

The other Profile-card fields persist elsewhere: tagline/biography in #2502, and role/avatar/badges/visibility have no backend yet and are their own upcoming stories (out of scope here).

This follows the same profileFormPATCH /api/v1/users/me/ pattern as #2502 (built fresh on develop, not a code dependency). Once #2502 merges, the two profileForm definitions will be reconciled into a single save().

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@javiercoronadonarvaez Thanks for the detailed context — that makes sense. Since the "Save Changes" button never actually persisted the other Profile-card fields before this PR (tagline/biography handled separately via #2502, and role/avatar/badges/visibility have no backend yet), save() scoped to profile_links isn't a regression. I'll withdraw this concern.


✏️ Learnings added
Learnt from: javiercoronadonarvaez
Repo: boostorg/website-v2 PR: 2514
File: templates/v3/user_profile_edit.html:145-145
Timestamp: 2026-07-02T16:29:33.677Z
Learning: In `templates/v3/user_profile_edit.html`, the "Save Changes" button in the Profile card previously never persisted the Profile-card fields (it was a plain submit with no backing view logic for those fields). This PR (boostorg/website-v2 `#2514`) introduces the first real persistence for that card, scoped only to `profile_links` via the `window.profileForm` Alpine component's `save()` method, which PATCHes `/api/v1/users/me/`. Tagline/biography persistence is handled separately (see PR `#2502`, which uses a similar `profileForm` → `PATCH /api/v1/users/me/` pattern but was built independently on `develop`). Role/avatar/badges/visibility fields have no backend persistence yet and are out of scope. Once `#2502` merges, the two separate `profileForm` Alpine component definitions are expected to be reconciled into a single `save()` implementation that persists all profile fields together.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread templates/v3/user_profile_edit.html
Comment thread templates/v3/user_profile_edit.html Outdated
Comment thread users/forms.py Outdated
Comment thread users/models.py
@herzog0
herzog0 self-requested a review July 2, 2026 18:41
@julhoang
julhoang self-requested a review July 2, 2026 19:27

@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/views.py (1)

111-115: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle empty profile_links before iterating the formset
V3UserProfileForm.__init__ only creates self.link_formset inside if links:, but later always iterates self.link_formset. Users with no profile links will hit an AttributeError on the profile edit page.

🤖 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 111 - 115, V3UserProfileForm currently only sets
link_formset inside the conditional in __init__, but other code always iterates
it, so users with no profile_links can hit an AttributeError. Update
V3UserProfileForm so link_formset is always initialized, even when user_links is
empty, and make the iteration logic in the form initialization/render path
safely handle an empty list using the V3UserProfileForm and self.link_formset
symbols as the main fix points.
🤖 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/views.py`:
- Around line 111-115: V3UserProfileForm currently only sets link_formset inside
the conditional in __init__, but other code always iterates it, so users with no
profile_links can hit an AttributeError. Update V3UserProfileForm so
link_formset is always initialized, even when user_links is empty, and make the
iteration logic in the form initialization/render path safely handle an empty
list using the V3UserProfileForm and self.link_formset symbols as the main fix
points.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc893060-6507-4c9f-b62e-723ee0d6d125

📥 Commits

Reviewing files that changed from the base of the PR and between b2325e5 and af17081.

📒 Files selected for processing (8)
  • core/context_processors.py
  • templates/v3/includes/_field_text.html
  • templates/v3/user_profile_edit.html
  • users/forms.py
  • users/serializers.py
  • users/tests/test_forms.py
  • users/tests/test_serializers.py
  • users/views.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • users/forms.py
  • core/context_processors.py
  • users/serializers.py
  • templates/v3/includes/_field_text.html
  • templates/v3/user_profile_edit.html

@herzog0
herzog0 removed their request for review July 7, 2026 17:32
@herzog0 herzog0 self-assigned this Jul 7, 2026

@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 @herzog0 , I just have 1 main request to validate the Slack username / user ID field and 1 nit suggestion to add https validation to server-side as well. Thank you!

Comment thread users/migrations/0022_user_profile_links.py
Comment thread users/forms.py Outdated
Comment thread templates/v3/user_profile_edit.html
Comment thread templates/v3/user_profile_edit.html
@herzog0
herzog0 requested a review from julhoang July 8, 2026 16:21
@herzog0
herzog0 force-pushed the javiercoronarv/edit-user-profile-links branch from 36655c7 to 6209ff9 Compare July 8, 2026 16:24

@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 @herzog0 ! I think the error text for the GitHub and Website fields are currently displayed next to the button instead of underneath the input fields. Would you mind directing error text to the correct slots?

Image

Edit: I see that removing the Slack display shortener was intentional, all good!

Thanks a lot for your help here!

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

Looks great to me, thank you for all the updates! 🙌

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

QA Approved

@herzog0
herzog0 force-pushed the javiercoronarv/edit-user-profile-links branch from d17dd96 to 80214bc Compare July 16, 2026 14:35
feat: expose and prefill profile_links via CurrentUser API and edit view

feat: persist links on Save Changes via PATCH to /api/v1/users/me/ endpoint

fix: redirect to http://localhost:8000/users/me/?edit=True when clicking Edit Profile button

feat: persist profile links on Save Changes with inline validation

fix: normalize Slack input to avoid double-prefixed team URL

fix: validate profile links as real HTTPS URLs, not just a prefix

fix: guard _display_link_value against non-string profile_links values

fix: help text matches actual functionality

fix: reject non-dict profile_links payloads in CurrentUserSerializer

fix: match migration help_text to the profile_links model field

fix: restore link length validation dropped by the alpine_error rendering path

fix: key profile link inputs by data-link-type instead of array position

fix: match Slack prefix stripping case-insensitivity between JS and Python

fix: build edit-profile CTA URL in the header context processor, not the template

feat: increase url max length

fix: clear saved-links indicator when a link is edited afterward

fix: allowlist profile_links keys and bound value length in API

fix: add aria-label to profile link value inputs

fix: dedupe edit-profile CTA URL construction into a shared helper

fix: pass SLACK_TEAM_URL_PREFIX from Python instead of hardcoding in JS

test: cover Slack link normalization and profile_links validation

fix: lint

fix: build Slack profile links as messages/<username> URLs

fix: clarify Slack placeholder refers to the CPPLang workspace

fix: shorten Slack placeholder text

fix: lint

fix: enforce secure URL check for profile links on backend

fix: build Slack profile links via app_redirect using the Member ID

fix: validate Slack link format on frontend and backend

fix: lint

fix: match profile_links migration help_text to model field

fix: escape Slack URL prefix for JS context and surface save errors in UI

feat: switch Slack profile link to cpplang.slack.com/team/<id> scheme

fix: lint

fix: make Slack link placeholder more intuitive

fix: show full Slack URL in edit form and use CPPLang-specific validation messages

fix: route backend link validation errors to their own field, not the save-button banner
@herzog0
herzog0 force-pushed the javiercoronarv/edit-user-profile-links branch from 80214bc to e8be1cd Compare July 16, 2026 14:37
@herzog0
herzog0 merged commit a53bcb4 into develop Jul 16, 2026
5 checks passed
@herzog0
herzog0 deleted the javiercoronarv/edit-user-profile-links branch July 16, 2026 16:32
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 - Profile Links

4 participants