Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion api/dashboard/profile/profile_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
UserLvlLink,
UserIgLvlLink,
)
from db.user import User, UserSettings, Socials
from db.user import User, UserSettings, Socials, UserProfile
from utils.exception import CustomException
from utils.permission import JWTUtils
from utils.types import (
Expand Down Expand Up @@ -577,3 +577,80 @@ def get_college_name(self, obj):
org_type = self._get_org_type(obj)
user_org_link = self._get_user_org_link(obj, org_type)
return user_org_link.org.title if user_org_link and user_org_link.org else None


class UserProfileSerializer(serializers.ModelSerializer):
user_id = serializers.CharField(source="user.id", read_only=True)
full_name = serializers.CharField(source="user.full_name", read_only=True)
email = serializers.CharField(source="user.email", read_only=True)
muid = serializers.CharField(source="user.muid", read_only=True)
profile_pic = serializers.SerializerMethodField()

class Meta:
model = UserProfile
Comment on lines +582 to +590

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

This file defines UserProfileSerializer twice. The second definition overwrites the existing UserProfileSerializer (used for the main User profile response), which will break endpoints that still expect the original serializer. Rename the new serializer (e.g., UserProfileEnhancedSerializer) and update usages accordingly.

Copilot uses AI. Check for mistakes.
fields = [
"user_id",
"full_name",
"email",
"muid",
"profile_pic",
"bio",
"projects",
"experience",
"created_at",
"updated_at",
]
read_only_fields = [
"user_id",
"full_name",
"email",
"muid",
"profile_pic",
"created_at",
"updated_at",
]

def get_profile_pic(self, obj):
return obj.user.profile_pic


class UserProfileUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
fields = [
"bio",
"projects",
"experience",
]

def validate_projects(self, value):
if not isinstance(value, list):
raise serializers.ValidationError("Projects must be a list")

for project in value:
if not isinstance(project, dict):
raise serializers.ValidationError("Each project must be an object")

required_fields = ["title", "link", "description", "tags"]
for field in required_fields:
if field not in project:
raise serializers.ValidationError(f"Project must have '{field}' field")

return value

def validate_experience(self, value):
if not isinstance(value, list):
raise serializers.ValidationError("Experience must be a list")

for exp in value:
if not isinstance(exp, dict):
raise serializers.ValidationError("Each experience entry must be an object")

return value
Comment on lines +626 to +649

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

validate_projects/validate_experience reject None, but the model fields are null=True. This makes it impossible to clear these fields via PATCH by sending null. Either disallow nulls at the model/serializer level, or accept None here (and configure the serializer fields with allow_null=True).

Copilot uses AI. Check for mistakes.

def update(self, instance, validated_data):
instance.bio = validated_data.get("bio", instance.bio)
instance.projects = validated_data.get("projects", instance.projects)
instance.experience = validated_data.get("experience", instance.experience)
instance.save()
return instance
106 changes: 106 additions & 0 deletions api/dashboard/profile/profile_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
UserEndgoals,
UserRoleLink,
UserSettings,
UserProfile,
)
from utils.permission import CustomizePermission, JWTUtils
from utils.response import CustomResponse
Expand Down Expand Up @@ -726,3 +727,108 @@ def get(self, request, muid):

serializer = profile_serializer.UserPermuteSerializer(user, many=False)
return CustomResponse(response=serializer.data).get_success_response()


class UserProfileEnhancedAPI(APIView):
"""
User Profile Enhancement API

Handles extended user profile information including bio, projects, and experience.

Endpoints:
GET /api/v1/dashboard/profile/user-profile/ - Fetch user profile
PATCH /api/v1/dashboard/profile/user-profile/ - Update user profile
"""

authentication_classes = [CustomizePermission]

def get(self, request):
"""
Fetch full user profile with bio, projects, and experience

Returns:
User profile with all details
"""
user_id = JWTUtils.fetch_user_id(request)
user = User.objects.filter(id=user_id).first()

if not user:
return CustomResponse(
general_message="User not found"
).get_failure_response()

# Get or create user profile
user_profile, created = UserProfile.objects.get_or_create(
user=user,
defaults={"created_by_id": user_id, "updated_by_id": user_id}
)

serializer = profile_serializer.UserProfileSerializer(user_profile, many=False)

return CustomResponse(response=serializer.data).get_success_response()

def patch(self, request):
"""
Update user profile fields (bio, projects, experience)

Request body:
{
"bio": "User biography",
"projects": [
{
"title": "Project Title",
"link": "https://github.com/...",
"description": "Project description",
"tags": ["React", "Firebase"]
}
],
"experience": [
{
"company": "Company Name",
"position": "Position",
"duration": "2020-2023"
}
]
}

Returns:
Updated user profile
"""
user_id = JWTUtils.fetch_user_id(request)
user = User.objects.filter(id=user_id).first()

if not user:
return CustomResponse(
general_message="User not found"
).get_failure_response()

# Get or create user profile
user_profile, created = UserProfile.objects.get_or_create(
user=user,
defaults={"created_by_id": user_id, "updated_by_id": user_id}
)

serializer = profile_serializer.UserProfileUpdateSerializer(
user_profile, data=request.data, partial=True
)

if serializer.is_valid():
serializer.save()

Comment on lines +805 to +817

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

The profile update flow never updates updated_by on UserProfile. get_or_create() sets updated_by_id only on creation, and UserProfileUpdateSerializer.update() doesn’t set it on subsequent updates, so audit data becomes stale. Pass the current user_id into the serializer (via context) and set instance.updated_by_id during update.

Copilot uses AI. Check for mistakes.
DiscordWebhooks.general_updates(
WebHookCategory.USER_NAME.value,
WebHookActions.UPDATE.value,
user_id,
)

response_serializer = profile_serializer.UserProfileSerializer(
user_profile, many=False
)
return CustomResponse(
response=response_serializer.data
).get_success_response()

return CustomResponse(
general_message="Invalid data",
error=serializer.errors
).get_failure_response()
2 changes: 1 addition & 1 deletion api/dashboard/profile/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
urlpatterns = [
path("", profile_view.UserProfileEditView.as_view()),
path("badges/<str:muid>", profile_view.BadgesAPI.as_view()),
path("user-profile/", profile_view.UserProfileAPI.as_view()),
path("user-profile/", profile_view.UserProfileEnhancedAPI.as_view(), name="user-profile-enhanced"),
path("ig-edit/", profile_view.UserIgEditView.as_view()),
path("user-profile/<str:muid>/", profile_view.UserProfileAPI.as_view()),
# path('edit-user-profile/', profile_view.UserProfileAPI.as_view()),
Expand Down
18 changes: 18 additions & 0 deletions db/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,21 @@ class Meta:
managed = False

db_table = 'user_coupon_link'


class UserProfile(models.Model):
id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='user_profile_user')
bio = models.TextField(max_length=500, blank=True, null=True)
projects = models.JSONField(default=list, blank=True, null=True)
experience = models.JSONField(default=list, blank=True, null=True)
updated_by = models.ForeignKey(User, on_delete=models.SET(settings.SYSTEM_ADMIN_ID), db_column='updated_by',
related_name='user_profile_updated_by')
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.SET(settings.SYSTEM_ADMIN_ID), db_column='created_by',
related_name='user_profile_created_by')
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
managed = False
db_table = 'user_profile'