Add profile photos so avatars can show the student's face - #68
Merged
Merged
Conversation
Parents can add a photo per student profile in the profile editor. Profile avatars (header chip, switcher list, profiles grid) render the photo when present and fall back to the first-letter initial otherwise. Backend stores the S3 key in Profile.photo_filename (migration 0049) and exposes a short-lived presigned photo_url on ProfileSerializer and OwnProfileSerializer; create/update accept multipart photo uploads and remove_photo=true to clear. Image upload helpers are shared with chat image uploads via bots.services.images. Frontend adds a ProfileAvatar component, photo pick/remove in the profile editor (expo-image-picker, square crop), and multipart upload in upsertProfile; text-only saves stay on the JSON path.
There was a problem hiding this comment.
🟡 Changes recommended
A few concrete error-handling and request-construction issues can lead to unintended behavior (500s on upload failures, accidental email clearing in multipart updates, and a non-standard image MIME type).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds end-to-end support for per-student profile photos so avatars can render a face when present (and fall back to the initial otherwise), spanning backend storage/serialization + frontend editing and avatar rendering.
Changes:
- Backend: store S3 key on
Profile.photo_filename, expose presignedphoto_url, and accept multipartphotouploads /remove_photo=trueon create & update. - Frontend: introduce
ProfileAvatarand wire it into the switcher and profiles list; add photo pick/change/remove to the profile editor and refresh selected-profile cache after save. - Tests: add backend coverage for photo behaviors plus new frontend unit tests for avatar/editor/multipart API behavior.
File summaries
| File | Description |
|---|---|
| front/components/ProfileSwitcher.tsx | Uses ProfileAvatar for chip + options list. |
| front/components/ProfileAvatar.tsx | New reusable avatar component (photo or initial fallback). |
| front/components/tests/ProfileAvatar-test.tsx | Tests initial vs photo rendering. |
| front/app/parent/profilesList.tsx | Renders photo avatar on profile cards when available. |
| front/app/parent/profileEditor.tsx | Adds photo pick/change/remove UI + multipart save path + selected-profile refresh. |
| front/app/tests/profileEditorPhoto-test.tsx | Tests editor behaviors for add/remove/unchanged photo flows. |
| front/api/profiles.ts | Adds photo_url to type and multipart upload/remove support in upsertProfile. |
| front/tests/api/profiles.test.ts | Adds coverage for multipart vs JSON behavior in upsertProfile. |
| back/bots/viewsets/profile_viewset.py | Hooks photo upload/remove into create/update via shared helper. |
| back/bots/views/get_chat_response.py | Refactors chat image upload to reuse shared image helpers/constants. |
| back/bots/tests/test_profile_photo.py | New backend tests for profile photo create/update/remove + permissions. |
| back/bots/services/images.py | New shared image validation/compress/upload + presigned URL helpers. |
| back/bots/serializers/profile_serializer.py | Adds photo_url SerializerMethodField to profile serializers. |
| back/bots/models/profile.py | Adds photo_filename to Profile. |
| back/bots/migrations/0049_profile_photo_filename.py | Migration adding photo_filename column. |
Review details
Suppressed comments (1)
back/bots/views/get_chat_response.py:63
compress_and_upload_image()can throw (e.g., invalid image bytes with a valid extension, S3 errors). Since this view doesn’t catch exceptions, an upload failure becomes a 500. Wrap the call and return a 400 with a clear error message.
if file.size > MAX_PHOTO_BYTES:
return JsonResponse({'error': 'File size exceeds 20MB limit'}, status=400)
if not allowed_file(file.name):
return JsonResponse({'error': 'Invalid file type'}, status=400)
filename = compress_and_upload_image(file)
- Files reviewed: 15/15 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+59
to
+64
| def upload_validated_photo(uploaded_file): | ||
| """Validate then upload; raise DRF ValidationError keyed `photo` on reject.""" | ||
| error = validate_uploaded_photo(uploaded_file) | ||
| if error: | ||
| raise ValidationError({'photo': error}) | ||
| return compress_and_upload_image(uploaded_file) |
Comment on lines
+54
to
+56
| formData.append('name', profile.name); | ||
| formData.append('oauth_email', profile.oauth_email?.trim() ? profile.oauth_email.trim() : ''); | ||
| if (profile.deleted_at) { |
Comment on lines
+66
to
+72
| const fileUri = opts.photoUri; | ||
| const fileType = fileUri.split('.').pop()?.toLowerCase() || 'jpeg'; | ||
| formData.append('photo', { | ||
| uri: fileUri, | ||
| name: `profile-photo.${fileType}`, | ||
| type: `image/${fileType}`, | ||
| } as any); |
Comment on lines
+218
to
+224
| <ProfileAvatar | ||
| profile={{ name: profile.name || "?", photo_url: null }} | ||
| size={96} | ||
| backgroundColor={iconColor} | ||
| testID="profile-photo-preview" | ||
| /> | ||
| )} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Parents can now add a photo to each student profile in the profile editor (Add/Change/Remove photo, square crop via expo-image-picker). Profile avatars — header chip, switcher list, profiles grid — render the photo when present and fall back to the first-letter initial otherwise.
Backend
Profile.photo_filename(migration 0049) stores the S3 key; serializers expose short-lived presignedphoto_urlon bothProfileSerializerandOwnProfileSerializer(teen self-read).photouploads andremove_photo=trueto clear; invalid type rejected 400 keyedphoto. Teen sessions stay blocked (403).bots.services.imagesand shared with chat image uploads (no behavior change there).Frontend
ProfileAvatarcomponent (photo or initial fallback) used byProfileSwitcherandprofilesList.upsertProfilesends multipart only when the photo changed; text-only saves stay on the JSON path.Verification
pytest bots/tests/), incl. 8 newtest_profile_photo.py;ruff checkclean.tsc --noEmitclean; eslint 0 errors (no new warnings).bots-worktrees/front-profile-photo, branchfeature/profile-photos.Demo (Playwright, web build)
Walkthrough video (login → header chip → switcher → profiles grid → editor photo pick):
Header chip shows Maya's face instead of "M":
Switcher: Maya's photo next to Leo's initial fallback:
Profiles grid: photo card vs generic icon card:
Editor with saved photo, Change/Remove actions: