feat(accounts): account profile view backend — contact_email + PATCH /v2/users/me/ (GUNDI-5347)#427
Conversation
- UserDetailsUpdateSerializer.update wrapped in transaction.atomic so a failed profile save reverts the user save. - add_account: collapse to a single bool return, add HTTP timeout and RequestException handling; drop dead JsonResponse branch and the unused user param on get_password_reset_link. - add_or_create_user_in_org: cache the org-members Group lookup and clean up control flow. - Admin: rename shadowing ModelAdmin to AccountProfileAdmin and add list_select_related to avoid N+1 on the list view. - AccountProfile.contact_email: add help_text documenting independence from the auth provider email. - Docstrings on UserWorkspaceSerializer, UserDetailsUpdateSerializer and UsersView; comment the query-count budget. - Tests: add a max_num_queries regression test for /v2/users/me/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds backend support for an Account Profile view by introducing a user-controlled contact_email, expanding the /v2/users/me/ payload to include profile + workspace membership data, and adding a self-service PATCH /v2/users/me/ for updating profile fields. This also refactors Keycloak integration into a dedicated module in preparation for an Auth0 migration.
Changes:
- Add
AccountProfile.contact_email(nullable) plus Django admin/search support and a migration. - Expand
GET /v2/users/me/response to includefirst_name,last_name,contact_email, andworkspaces[]; addPATCH /v2/users/me/with atomic updates acrossUser+AccountProfile. - Extract Keycloak logic into
accounts/keycloak.py, update call sites, and adjust tests/mocks accordingly.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| cdip_admin/emails/utils.py | Updates Keycloak import path for password reset link helper. |
| cdip_admin/api/v2/views.py | Enables user self-service PATCH via UpdateModelMixin and action-based serializer selection. |
| cdip_admin/api/v2/urls.py | Routes PATCH /v2/users/me/ to partial_update. |
| cdip_admin/api/v2/serializers.py | Adds workspace serializer; extends /me retrieve fields; adds atomic PATCH serializer with retrieve-shaped response. |
| cdip_admin/api/v2/tests/test_users_api.py | Adds coverage for expanded /me shape, PATCH behaviors, auth requirement, and N+1 guard. |
| cdip_admin/api/v2/tests/test_organizations_api.py | Updates Keycloak mock patch target to new module. |
| cdip_admin/accounts/utils.py | Switches to new accounts.keycloak.add_account and refactors org-member group assignment. |
| cdip_admin/accounts/models.py | Adds contact_email field to AccountProfile. |
| cdip_admin/accounts/migrations/0017_accountprofile_contact_email.py | Introduces DB schema change for contact_email. |
| cdip_admin/accounts/keycloak.py | New Keycloak integration module with timeout and network-error handling. |
| cdip_admin/accounts/admin.py | Exposes contact_email in admin list/search and edits fieldsets. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- emails/utils: drop unused get_password_reset_link import. - migration 0017: include help_text on contact_email AddField so it matches the model and makemigrations --check stays clean. - keycloak.add_account: replace response.ok with explicit 2xx range check (response.ok is True for 3xx too, contradicting the docstring). - accounts.utils: use DjangoGroups.ORGANIZATION_MEMBER.value for the group lookup, matching conftest.py and core/permissions.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- accounts/utils: import the keycloak module instead of binding the function at import time. With `from .keycloak import add_account`, patching `accounts.keycloak.add_account` in tests no longer intercepted the call inside `add_or_create_user_in_org`, so the invite tests would issue real outbound HTTP to Keycloak. Switching to `from . import keycloak` and `keycloak.add_account(...)` makes the patch resolve at call-time, restoring test isolation. - keycloak.add_account: use `logger.exception` (preserves traceback) in the RequestException handler, matching the convention used in event_consumers and integrations/tasks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Possible dead code: This PR removes the Suggest either wiring it up where it was intended to be used, or dropping it so we don't carry dead code into the Auth0 migration. (Worth a quick double-check that it isn't referenced from a template or other non- 🤖 Generated with Claude Code |
|
Nice work on two things in the Keycloak refactor that are easy to miss 👏
🤖 Generated with Claude Code |
|
Data-model follow-up filed: GUNDI-5377 A design review of the The one genuine, pre-existing risk surfaced is that the "every User has exactly one AccountProfile" invariant is unenforced (the OIDC login path creates a User with no profile), which is why the codebase carries scattered 🤖 Generated with Claude Code |
After removing the unused import in emails/utils.py (commit 9359a51), get_password_reset_link had no callers — and no templates reference a `password_reset_link` context variable either. The invite email tells the user to click "Forgot Password?" in the portal UI; there is no email-embedded reset link. Carrying a Keycloak-specific URL builder into the Auth0 migration is exactly the kind of dead weight we want to leave behind. If a reset link is ever needed again, it should be added via the AuthProvider abstraction at that time. Also drops the now-unused KEYCLOAK_CLIENT constant. Addresses chrisdoehring's review comment on PR #427. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Good catch — confirmed dead code. Verified there's no Dropped |
|
Thanks for calling those out — appreciated. Both were genuinely latent: the |
|
Acknowledged and thanks for the design review. Agreed the |
Summary
Backend support for the Account Profile View (GUNDI-5347, sub-ticket of the epic GUNDI-5247 — see the backend implementation plan).
AccountProfile.contact_email(EmailField, nullable). Independent ofUser.email, which stays managed by the auth provider — needed for the upcoming Auth0 migration.GET /v2/users/me/now also returnsfirst_name,last_name,contact_email, andworkspaces[](each withid,workspace_id,name,role) so the frontend can populate the whole Account Page with a single request.PATCH /v2/users/me/to let users updatefirst_name,last_name,contact_email— transactional so a partial failure can't leave the User and AccountProfile out of sync.accounts/utils.pyintoaccounts/keycloak.py(prep for the Auth0 swap).add_accountnow has an HTTP timeout, a single bool return, and proper network-error handling.API changes
/v2/users/me/first_name,last_name,contact_email,workspaces[]. Existing fields unchanged./v2/users/me/{first_name, last_name, contact_email}. Returns the retrieve shape.Frontend can populate the Account Page from a single
GET /v2/users/me/:```json
{
"id": ...,
"username": "...",
"first_name": "...",
"last_name": "...",
"contact_email": "user@example.com" | null,
"workspaces": [
{"id": 42, "workspace_id": "<org_uuid>", "name": "Workspace A", "role": "admin"}
]
}
```
Out of scope (deferred to sibling sub-tickets of the epic)
Test plan
🤖 Generated with Claude Code