Skip to content

feat(user): add user profile picture update functionality#306

Open
AnujChhikara wants to merge 3 commits intodevelopfrom
anuj/feat-image-upload
Open

feat(user): add user profile picture update functionality#306
AnujChhikara wants to merge 3 commits intodevelopfrom
anuj/feat-image-upload

Conversation

@AnujChhikara
Copy link
Copy Markdown
Contributor

@AnujChhikara AnujChhikara commented Apr 17, 2026

Date: 17 April, 2026

Developer Name: @AnujChhikara


Issue Ticket Number

Tech Doc Link

  • NA

Business Doc Link

  • NA

Description

  • Introduced a new endpoint to update the user's profile picture.
  • Added a serializer to validate the uploaded image.
  • Implemented Cloudinary service for image upload.
  • Updated user repository to store the picture URL.
  • Enhanced user view to handle profile picture updates with appropriate error handling.
  • Updated environment configuration for Cloudinary integration.
  • Added integration tests for the new profile picture update feature.

Documentation Updated?

  • Yes
  • No

Under Feature Flag

  • Yes
  • No

Database Changes

  • Yes
  • No

Breaking Changes

  • Yes
  • No

Development Tested?

  • Yes
  • No

Screenshots

Screenshot 1
user.profile.todo.mp4

Test Coverage

Screenshot 1

Additional Notes

@AnujChhikara AnujChhikara self-assigned this Apr 17, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b79a66c-13ab-4b42-8472-87e0adbad948

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR introduces profile picture upload functionality by integrating Cloudinary. New components include environment variable configuration, a Cloudinary service for image uploads, a serializer for file validation, a repository method for persisting picture URLs, and a new API endpoint to handle profile picture updates with full integration tests.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
.env.example, requirements.txt
Added Cloudinary environment variables and pinned cloudinary==1.37.0 dependency.
Request Validation
todo/serializers/update_profile_serializer.py
New serializer enforces presence, restricts uploads to JPEG/PNG/WebP formats, and caps file size at 5MB.
External Service Integration
todo/services/cloudinary_service.py
New service abstracts Cloudinary uploads, validates config from environment variables, derives folder/public_id paths, and returns secure URLs.
Data Persistence
todo/repositories/user_repository.py
Added update_picture_by_id method to update user document's picture field and updated_at timestamp.
API Endpoint
todo/views/user.py, todo/urls.py
New UserDetailView with PATCH endpoint orchestrates validation, Cloudinary upload, and repository persistence; new route /users/profile mapped to this view.
Tests
todo/tests/integration/test_user_profile_api.py
Integration tests verify authentication enforcement and picture persistence by mocking Cloudinary uploads and asserting profile data is returned correctly.

Sequence Diagram

sequenceDiagram
    participant Client
    participant UserDetailView
    participant UpdateProfileSerializer
    participant CloudinaryService
    participant Cloudinary
    participant UserRepository
    participant Database

    Client->>UserDetailView: PATCH /users/profile (multipart file)
    UserDetailView->>UpdateProfileSerializer: validate(request.data)
    UpdateProfileSerializer->>UpdateProfileSerializer: validate_picture(file)<br/>(format, size check)
    UpdateProfileSerializer-->>UserDetailView: validated picture file
    
    UserDetailView->>CloudinaryService: upload_image(file_data,<br/>user_id, image_name)
    CloudinaryService->>CloudinaryService: _require_config()
    CloudinaryService->>Cloudinary: uploader.upload(BytesIO,<br/>folder, public_id)
    Cloudinary-->>CloudinaryService: {secure_url, ...}
    CloudinaryService-->>UserDetailView: secure_url
    
    UserDetailView->>UserRepository: update_picture_by_id(user_id,<br/>picture_url)
    UserRepository->>Database: findOneAndUpdate(_id,<br/>picture, updated_at)
    Database-->>UserRepository: updated_user_doc
    UserRepository-->>UserDetailView: UserModel
    
    UserDetailView-->>Client: 200 OK {id, email,<br/>name, picture}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A picture's worth a thousand hops,
Now Cloudinary never stops!
Upload, validate, persist with pride,
Users' profiles bloom far and wide. ✨📸

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(user): add user profile picture update functionality' accurately summarizes the main change—implementing user profile picture update capability—and aligns with all file modifications across the changeset.
Description check ✅ Passed The pull request description clearly outlines the changes made, including the new endpoint, serializer, Cloudinary service, repository updates, view enhancements, environment configuration, and integration tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch anuj/feat-image-upload

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@requirements.txt`:
- Line 32: Replace the pinned dependency "cloudinary==1.37.0" in
requirements.txt with "cloudinary==1.44.1"; after updating the version string,
regenerate any dependency lockfile or run your dependency tooling (pip install
-r requirements.txt or pip-compile / poetry lock as applicable) and run the test
suite to ensure compatibility with the Cloudinary v1.44.1 upgrade.

In `@todo/repositories/user_repository.py`:
- Around line 100-115: The Mongo-only update in update_picture_by_id causes
Postgres drift; mirror the dual-write used in create_or_update by invoking the
same EnhancedDualWriteService update flow after the Mongo update. Specifically,
in update_picture_by_id (after collection.find_one_and_update returns a result
and before returning UserModel(**result)), call the EnhancedDualWriteService
method used by create_or_update to sync updates to Postgres with the user_id and
the changed fields (picture and updated_at); handle any DualWrite errors
consistently with create_or_update (log/raise or surface failure) so both stores
stay consistent and errors are not swallowed.
- Line 104: The code constructs a PyObjectId from user_id without validation, so
malformed IDs raise and return HTTP 500; wrap the PyObjectId(user_id)
construction in a try/except (or validate user_id up front) inside the method
that uses it and on error raise the same repository-level error (e.g.,
UserNotFoundException or a validation error) to mirror get_by_id behavior;
reference the PyObjectId(user_id) call and ensure you raise
UserNotFoundException (or the repository's validation exception) instead of
letting the raw exception propagate.

In `@todo/serializers/update_profile_serializer.py`:
- Around line 12-13: The validate_picture method contains a dead check: remove
the unreachable "if not value: raise serializers.ValidationError('picture file
is required')" because FileField(required=True) already enforces presence;
instead validate the actual file content-type and size, and if you need stronger
guarantees validate magic bytes (e.g., open with Pillow or check file header)
rather than trusting value.content_type which is client-supplied; keep any
remaining checks to raise serializers.ValidationError when the file fails
content/size/type validation.

In `@todo/services/cloudinary_service.py`:
- Around line 4-22: The _require_config() in CloudinaryService reads
CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET but never
applies them, so uploader.upload will fail; fix by calling
cloudinary.config(cloud_name=..., api_key=..., api_secret=...) with the values
returned from _require_config() before any upload (or move that config call to
application startup). Update CloudinaryService (where _require_config() is
invoked) to use its return tuple to call cloudinary.config(...) prior to calling
uploader.upload(), ensuring credentials are actually applied; keep the
cloudinary import and remove any unused env reads if you centralize config at
app startup.

In `@todo/tests/integration/test_user_profile_api.py`:
- Around line 31-44: The test test_update_profile_picture_persists_picture_url
fails because using BytesIO(b"fake_image_data") yields content_type
"application/octet-stream" which validate_picture rejects against
ALLOWED_IMAGE_TYPES, so replace the file fixture with a Django
SimpleUploadedFile that sets an explicit image content_type (e.g., "image/png")
so the serializer's validate_picture accepts it and
CloudinaryService.upload_image is invoked; update the patch call to send the
SimpleUploadedFile instead of BytesIO while keeping the existing patch of
CloudinaryService.upload_image.

In `@todo/views/user.py`:
- Around line 159-169: The current broad except in the profile image upload
masks configuration/validation errors and returns raw error strings; update the
try/except around CloudinaryService.upload_image to catch specific exceptions
(e.g., APIException from CloudinaryService._require_config and the
cloudinary.exceptions.* classes), log the full exception server-side using
logger.exception or similar, and return a generic client-facing Response like
{"message": "Failed to upload image"} with the appropriate 5xx status (keep
APIException/config errors as 500) without including str(e) in the payload;
ensure CloudinaryService.upload_image remains called as-is and only the
exception handling around it is changed.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 705b13b5-58d2-44d8-b667-eaf5e01ba9d5

📥 Commits

Reviewing files that changed from the base of the PR and between 5f8fa47 and 5a2b9de.

📒 Files selected for processing (8)
  • .env.example
  • requirements.txt
  • todo/repositories/user_repository.py
  • todo/serializers/update_profile_serializer.py
  • todo/services/cloudinary_service.py
  • todo/tests/integration/test_user_profile_api.py
  • todo/urls.py
  • todo/views/user.py

Comment thread requirements.txt Outdated
Comment thread todo/repositories/user_repository.py Outdated
Comment thread todo/repositories/user_repository.py
Comment thread todo/serializers/update_profile_serializer.py Outdated
Comment on lines +4 to +22
from cloudinary import uploader
import cloudinary

from todo.exceptions.auth_exceptions import APIException


class CloudinaryService:
@staticmethod
def _require_config() -> tuple[str, str, str]:
cloud_name = os.getenv("CLOUDINARY_CLOUD_NAME")
api_key = os.getenv("CLOUDINARY_API_KEY")
api_secret = os.getenv("CLOUDINARY_API_SECRET")

if not cloud_name or not api_key or not api_secret:
raise APIException(
"Cloudinary is not configured. Set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET."
)

return str(cloud_name), str(api_key), str(api_secret)
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 17, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

cloudinary python SDK configure credentials cloudinary.config vs CLOUDINARY_URL

💡 Result:

In the Cloudinary Python SDK, credentials can be configured in two primary ways: using the CLOUDINARY_URL environment variable or explicitly via cloudinary.config. The CLOUDINARY_URL environment variable is the recommended and most convenient method, especially for security. It follows the format cloudinary://api_key:api_secret@cloud_name and can include additional query parameters for other config options (e.g., ?secure=true). The SDK automatically detects and parses this variable upon import/initialization if present, populating the config with cloud_name, api_key, api_secret, and other params. No explicit config call is needed for basic setup. Use tools like python-dotenv to load it from a .env file. Example: # .env CLOUDINARY_URL=cloudinary://your_api_key:your_api_secret@your_cloud_name # In code from dotenv import load_dotenv load_dotenv import cloudinary # Credentials are auto-loaded; verify with: print(cloudinary.config.cloud_name) Alternatively, use cloudinary.config to set credentials programmatically, either directly or from other env vars. This overrides or supplements the auto-loaded config. Example: import cloudinary import os cloudinary.config( cloud_name="your_cloud_name", api_key=os.getenv("CLOUDINARY_API_KEY"), api_secret=os.getenv("CLOUDINARY_API_SECRET"), secure=True ) Both methods support all config params (e.g., secure, private_cdn). CLOUDINARY_URL is preferred for production as it centralizes secrets in env vars. Call config before importing uploader/api modules for full functionality. The global config is a singleton updated by both approaches.

Citations:


🏁 Script executed:

cat -n todo/services/cloudinary_service.py

Repository: RealDevSquad/todo-backend

Length of output: 1764


🏁 Script executed:

rg "cloudinary\.config" --type py

Repository: RealDevSquad/todo-backend

Length of output: 51


🏁 Script executed:

rg "CLOUDINARY_URL" --type py

Repository: RealDevSquad/todo-backend

Length of output: 51


Critical: Cloudinary is not configured — uploader.upload will fail in production

_require_config() reads the environment variables and returns them, but the return value is discarded and cloudinary.config() is never called. The Cloudinary SDK requires either a CLOUDINARY_URL environment variable (which is auto-parsed) or an explicit cloudinary.config(cloud_name=..., api_key=..., api_secret=...) call; with three separate CLOUDINARY_* variables and no config call, uploader.upload will raise an authentication error at runtime. The cloudinary import is also dead code until configuration is applied.

At line 32, _require_config() is called but its return value is ignored. The credentials are read but never used.

Proposed fix: Call cloudinary.config() with the environment variables before uploader.upload() is called:

Example fix
     `@staticmethod`
-    def _require_config() -> tuple[str, str, str]:
+    def _require_config() -> None:
         cloud_name = os.getenv("CLOUDINARY_CLOUD_NAME")
         api_key = os.getenv("CLOUDINARY_API_KEY")
         api_secret = os.getenv("CLOUDINARY_API_SECRET")

         if not cloud_name or not api_key or not api_secret:
             raise APIException(
                 "Cloudinary is not configured. Set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET."
             )
-
-        return str(cloud_name), str(api_key), str(api_secret)
+        cloudinary.config(
+            cloud_name=cloud_name,
+            api_key=api_key,
+            api_secret=api_secret,
+            secure=True,
+        )

Alternatively, move the configuration call to application startup to avoid reconfiguring on every upload.

🧰 Tools
🪛 GitHub Actions: Tests

[error] 5-5: ruff check reported unused import: cloudinary imported but unused (F401). Remove unused import: cloudinary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@todo/services/cloudinary_service.py` around lines 4 - 22, The
_require_config() in CloudinaryService reads CLOUDINARY_CLOUD_NAME,
CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET but never applies them, so
uploader.upload will fail; fix by calling cloudinary.config(cloud_name=...,
api_key=..., api_secret=...) with the values returned from _require_config()
before any upload (or move that config call to application startup). Update
CloudinaryService (where _require_config() is invoked) to use its return tuple
to call cloudinary.config(...) prior to calling uploader.upload(), ensuring
credentials are actually applied; keep the cloudinary import and remove any
unused env reads if you centralize config at app startup.

✅ Confirmed as addressed by @AnujChhikara

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

check now

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
cat -n todo/services/cloudinary_service.py

Length of output: 2016


@AnujChhikara, the fix looks good! The core issue is resolved:

  • The new _configure() method (Lines 24–31) correctly unpacks the tuple returned by _require_config() and passes the credentials to cloudinary.config(...).
  • upload_image now calls cls._configure() (Line 41) before invoking uploader.upload(), ensuring credentials are applied at runtime.
  • The cloudinary import is no longer dead code.

One optional improvement: consider adding secure=True to the cloudinary.config() call to ensure all asset URLs use HTTPS by default:

cloudinary.config(
    cloud_name=cloud_name,
    api_key=api_key,
    api_secret=api_secret,
    secure=True,  # ensures secure_url is always HTTPS
)

But that's a non-critical enhancement — the authentication issue is fully addressed. ✅


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@AnujChhikara can you try adding secure=True as well in config?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment thread todo/tests/integration/test_user_profile_api.py
Comment thread todo/views/user.py
- Introduced a new endpoint to update the user's profile picture.
- Implemented a serializer to validate the uploaded image.
- Integrated Cloudinary for image upload and storage.
- Updated user repository to handle picture URL updates.
- Added integration tests for the new profile picture update feature.
- Updated .env.example to include Cloudinary configuration variables.
- Added cloudinary dependency to requirements.txt.
@AnujChhikara AnujChhikara force-pushed the anuj/feat-image-upload branch from 5a2b9de to dfa9cdd Compare April 17, 2026 18:22
- Replaced BytesIO with SimpleUploadedFile for simulating image uploads in tests.
- Added tests for invalid file types and missing files during profile picture updates.
- Ensured proper handling of HTTP status codes for various scenarios.
Comment thread todo/services/cloudinary_service.py Outdated
Comment on lines +4 to +22
from cloudinary import uploader
import cloudinary

from todo.exceptions.auth_exceptions import APIException


class CloudinaryService:
@staticmethod
def _require_config() -> tuple[str, str, str]:
cloud_name = os.getenv("CLOUDINARY_CLOUD_NAME")
api_key = os.getenv("CLOUDINARY_API_KEY")
api_secret = os.getenv("CLOUDINARY_API_SECRET")

if not cloud_name or not api_key or not api_secret:
raise APIException(
"Cloudinary is not configured. Set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET."
)

return str(cloud_name), str(api_key), str(api_secret)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@AnujChhikara can you try adding secure=True as well in config?

- Set secure uploads to true for Cloudinary integration.
- Modified public ID generation to use only the image name, removing the user ID prefix.
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