diff --git a/api/dashboard/profile/profile_serializer.py b/api/dashboard/profile/profile_serializer.py index 4c898a28..bc42b3d8 100644 --- a/api/dashboard/profile/profile_serializer.py +++ b/api/dashboard/profile/profile_serializer.py @@ -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 ( @@ -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 + 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 + + 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 diff --git a/api/dashboard/profile/profile_view.py b/api/dashboard/profile/profile_view.py index d77d630f..5d7cb77e 100644 --- a/api/dashboard/profile/profile_view.py +++ b/api/dashboard/profile/profile_view.py @@ -26,6 +26,7 @@ UserEndgoals, UserRoleLink, UserSettings, + UserProfile, ) from utils.permission import CustomizePermission, JWTUtils from utils.response import CustomResponse @@ -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() + + 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() diff --git a/api/dashboard/profile/urls.py b/api/dashboard/profile/urls.py index 93f3ad3c..a961c518 100644 --- a/api/dashboard/profile/urls.py +++ b/api/dashboard/profile/urls.py @@ -5,7 +5,7 @@ urlpatterns = [ path("", profile_view.UserProfileEditView.as_view()), path("badges/", 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//", profile_view.UserProfileAPI.as_view()), # path('edit-user-profile/', profile_view.UserProfileAPI.as_view()), diff --git a/db/user.py b/db/user.py index ca597df1..c896bb8d 100644 --- a/db/user.py +++ b/db/user.py @@ -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'