feat: generate initials avatar as default profile image#38638
feat: generate initials avatar as default profile image#38638SantiagoSuHe wants to merge 2 commits into
Conversation
|
Thanks for the pull request, @SantiagoSuHe! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. 🔘 Update the status of your PRYour PR is currently marked as a draft. After completing the steps above, update its status by clicking "Ready for Review", or removing "WIP" from the title, as appropriate. Where can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
When a user has not uploaded a profile photo, generate a personalized JPEG avatar with their initials on a colored circle background instead of returning a generic static placeholder image. Images are generated on first request and cached in storage using a content-addressable key based on username + name. A name change automatically produces a new cache key and a fresh image on the next request. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8657283 to
0a196f5
Compare
|
|
||
|
|
||
| _AVATAR_COLORS = [ | ||
| '#1565C0', '#2E7D32', '#6A1B9A', '#C62828', '#E65100', |
There was a problem hiding this comment.
update to reference paragon / brand color options - may want to use the paragon label colors here (PR is still a draft so TBD)
There was a problem hiding this comment.
Hi @marcotuts, I've updated the color palette to use values from the Paragon light theme design tokens. The new palette draws from the primary, brand, success, info, and danger color families (two shades per family for variety). I excluded the warning/yellow shades since they don't provide sufficient contrast against white text (WCAG AA).
Here's a preview of the palette: https://coolors.co/palette/0a3055-9d0054-178253-006daa-c32d3a-476480-b6407f-15754b-006299-b02934
Do these colors look good to you, or would you suggest any adjustments?
Use hex values from the Paragon light theme design token system (primary, brand, success, info, and danger families) instead of arbitrary Material Design colors. Warning/yellow shades are excluded because they lack sufficient contrast against white text (WCAG AA). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update: feedback round and reconsidering the approachAfter a feedback round with @edschema, we identified some weaknesses in this PR's implementation. Sharing them here along with the alternatives we are considering. Context: how upstream works today (without this PR)There is a pre-existing bug, independent of this PR: an uploaded profile photo is never shown in the MFE headers. Headers only read Fixing it requires enabling hydration in each MFE: one extra accounts API call per logged-in user, per MFE they navigate to. Today's default avatar costs zero (a bundled SVG and static files), and the extra call would only benefit the minority of users with an uploaded photo. How this PR works, and where its weaknesses areThis PR generates the initials avatar as JPEGs (one per size in
Alternative approaches we are consideringBoth render the avatar on the client, matching today's zero-cost economics: initials derived from the username, a deterministic background color (hash against a theme-token palette), and a colored circle with text. No image is generated, stored, or fetched. They differ in where the logic lives: Option 1: implement the fallback locally at each avatar surface. The avatar generation flow would work like this, taking the header (the main surface) as the example:
The same small change is then repeated at the two other surfaces, where the username is also already in scope: discussions (post/comment/reply authors — the Drawback: the rendering logic (~15 lines: hash, palette, initial) gets duplicated in three repos. Option 2: add an initials variant to Paragon's We are looking for feedback on this analysis and opinions on which route would be the best way forward. Depending on that, this PR will likely be reworked or closed in favor of one of the client-side approaches. |
Summary
Open edX shows a generic static placeholder when a user has not uploaded a profile photo. This PR replaces that placeholder with a personalized avatar: a colored circle with the user's initials, similar to what Google and other platforms do.
A proposal for this feature was posted in the Open edX community forum for visibility: https://discuss.openedx.org/t/profile-avatar-modernization/19046
How it works
The avatar is generated in the backend rather than the frontend so that every consumer of
get_profile_image_urls_for_user()receives it automatically, with no changes needed in individual MFEs or templates. Generating a real JPEG means the image works anywhere profile photos already work, from Django-rendered pages to theedx-user-infocookie, without additional integration.Generation is lazy: nothing happens at account creation. The first time profile image URLs are requested for a user (for example, at login), the system calls
generate_initials_image(username, name). Pillow, already required byopenedx-platformfor resizing uploaded photos, draws a colored JPEG and saves it to storage under a key derived frommd5(username + name). Subsequent requests return the cached file directly. The initials come from the first letter of the user's first and last name, and the background color is deterministically derived from the username using the Paragon light theme design token palette, so the same user always gets the same color. If the user has no name, the first letter of their username is used instead.How the avatar changes over time:
edx-user-infocookie.has_profile_imagebecomesTrueand the uploaded photo is served instead.has_profile_imagegoes back toFalseand the initials avatar is served again.MFE headers require two additional steps outside the scope of this PR. When a MFE loads,
@edx/frontend-platformdecodes the JWT cookie into a basicauthenticatedUserobject that does not include profile image data. To surface the avatar in a MFE header, the MFE must passhydrateAuthenticatedUser: truetoinitialize(), which fetches the full account payload from the accounts API. Additionally, openedx/frontend-platform#885 mapsprofile_image.image_url_fullintoauthenticatedUser.avatarduring hydration. Both conditions are required: hydration without the mapping leaves the avatar field empty, and the mapping without hydration means the API call never happens.Companion PRs
openedx/frontend-app-profileProfileAvatarcomponent to render the URL returned by the backend instead of always showing the SVG placeholder whenhas_imageisfalseopenedx/frontend-platformprofile_image.image_url_fulltoauthenticatedUser.avatarduring hydration so MFE headers receive the avatar URLChanges
openedx/core/djangoapps/profile_images/images.pygenerate_initials_image(username, name)which generates a JPEG avatar for each configured size using Pillowmd5(username + name), generated only once per username/name combinationopenedx/core/djangoapps/user_api/accounts/image_helpers.py_get_default_profile_image_urls()to callgenerate_initials_image()instead of returning static placeholder URLsUserProfile.has_profile_imagesemantics are unchanged -- it remainsFalseuntil the user explicitly uploads a photoTesting
Added tests for
_get_initials(),_get_avatar_color(), andgenerate_initials_image()covering edge cases (empty name, None, whitespace, name change cache invalidation, storage caching behavior).Updated existing tests in
test_image_helpers.pyto mockgenerate_initials_imagefor the default image path.Dependencies
No new dependencies. Pillow is already a required dependency of
openedx-platform.Local MFE Testing
Tested locally against a Tutor dev environment with
frontend-platformPR #885 applied.frontend-app-account— Already hadhydrateAuthenticatedUser: trueupstream. Avatar works.frontend-app-catalog— Does not havehydrateAuthenticatedUser: trueupstream. After adding it, avatar works.frontend-app-gradebook— Does not havehydrateAuthenticatedUser: trueupstream. After adding it, avatar works.frontend-app-learner-dashboard— Does not havehydrateAuthenticatedUser: trueupstream. After adding it, avatar works.frontend-app-learner-record— Does not havehydrateAuthenticatedUser: trueupstream. After adding it, avatar works.frontend-app-admin-console— Does not havehydrateAuthenticatedUser: trueupstream. After adding it, avatar appears but renders at 500×500px — the Studio header renders<img>directly inside the button when a URL is set, bypassing Paragon's<Avatar>component which constrains the size. Needs a fix inopenedx/frontend-component-header.frontend-app-authoring— Does not havehydrateAuthenticatedUser: trueupstream. After adding it, same Studio header size bug as admin-console.frontend-app-admin-portal— Not testable locally (requires enterprise context).frontend-app-learner-portal-enterprise— Not testable locally (requires enterprise microservices).frontend-app-discussions— Not testable locally (forum service not included in the standard Tutor dev stack).