Skip to content

Add profile photos so avatars can show the student's face - #68

Merged
tpaulshippy merged 2 commits into
mainfrom
feature/profile-photos
Sep 8, 2026
Merged

tpaulshippy merged 2 commits into
mainfrom
feature/profile-photos

Conversation

@tpaulshippy

@tpaulshippy tpaulshippy commented Sep 8, 2026 •

Copy link
Copy Markdown
Owner

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 presigned photo_url on both ProfileSerializer and OwnProfileSerializer (teen self-read).
  • Create/update accept multipart photo uploads and remove_photo=true to clear; invalid type rejected 400 keyed photo. Teen sessions stay blocked (403).
  • Image upload helpers extracted to bots.services.images and shared with chat image uploads (no behavior change there).

Frontend

  • New ProfileAvatar component (photo or initial fallback) used by ProfileSwitcher and profilesList.
  • upsertProfile sends multipart only when the photo changed; text-only saves stay on the JSON path.
  • Editor refreshes the cached selected profile after save so the header chip updates immediately.

Verification

  • Backend: 278 passed (pytest bots/tests/), incl. 8 new test_profile_photo.py; ruff check clean.
  • Frontend: full jest suite 32 passed / 169 tests; tsc --noEmit clean; eslint 0 errors (no new warnings).
  • Worktree: bots-worktrees/front-profile-photo, branch feature/profile-photos.

Demo (Playwright, web build)

Walkthrough video (login → header chip → switcher → profiles grid → editor photo pick):

profile photo walkthrough

Header chip shows Maya's face instead of "M":

header chip with profile photo

Switcher: Maya's photo next to Leo's initial fallback:

profile switcher with photo and initial fallback

Profiles grid: photo card vs generic icon card:

profiles grid with photo card

Editor with saved photo, Change/Remove actions:

profile editor with photo

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.

Copilot AI 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.

🟡 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 presigned photo_url, and accept multipart photo uploads / remove_photo=true on create & update.
  • Frontend: introduce ProfileAvatar and 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 thread back/bots/services/images.py Outdated
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 thread front/api/profiles.ts
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 thread front/api/profiles.ts
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"
/>
)}
@tpaulshippy
tpaulshippy merged commit e3939c3 into main Sep 8, 2026
3 checks passed
@tpaulshippy
tpaulshippy deleted the feature/profile-photos branch September 8, 2026 14:55
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.

2 participants