diff --git a/.env.sample b/.env.sample index 158f7350..cc401799 100644 --- a/.env.sample +++ b/.env.sample @@ -38,4 +38,9 @@ PROTECTED_API_KEY = SYSTEM_ADMIN_ID = -LAUNCHPAD_ADMIN_EMAIL = \ No newline at end of file +LAUNCHPAD_ADMIN_EMAIL = + +DISCORD_CLIENT_ID=xxx +DISCORD_CLIENT_SECRET=xxx +DISCORD_GUILD_ID=xxx +DISCORD_BOT_TOKEN=xxx \ No newline at end of file diff --git a/api/register/register_views.py b/api/register/register_views.py index 386b41b5..df08f9c0 100644 --- a/api/register/register_views.py +++ b/api/register/register_views.py @@ -4,15 +4,110 @@ from db.organization import Country, Department, District, Organization, State, Zone from django.utils.decorators import method_decorator from db.task import InterestGroup -from db.user import Role, User +from db.user import Role, User, UserInterests from utils.response import CustomResponse from utils.types import OrganizationType -from utils.utils import send_template_mail from . import serializers from .register_helper import get_auth_token from django.views.decorators.cache import cache_page from django.core.cache import cache from mu_celery.task import send_email +from utils.permission import CustomizePermission, JWTUtils +from decouple import config +import requests +from mu_celery.task import onboard_user + +DISCORD_CLIENT_ID = config("DISCORD_CLIENT_ID") +DISCORD_CLIENT_SECRET = config("DISCORD_CLIENT_SECRET") +FR_DOMAIN_NAME = config("FR_DOMAIN_NAME") + + +class ConnectDiscordAPI(APIView): + def get(self, request): + if not JWTUtils.is_jwt_authenticated(request): + return CustomResponse( + general_message="Unauthorized access" + ).get_failure_response() + user_id = JWTUtils.fetch_user_id(request) + token = request.GET.get("code") + if not token: + return CustomResponse( + general_message="Invalid or no token given" + ).get_failure_response() + token_url = "https://discord.com/api/oauth2/token" + redirect_uri = f"{FR_DOMAIN_NAME}/dashboard/connect-discord/" + data = { + "client_id": DISCORD_CLIENT_ID, + "client_secret": DISCORD_CLIENT_SECRET, + "grant_type": "authorization_code", + "code": token, + "redirect_uri": redirect_uri, + } + headers = {"Content-Type": "application/x-www-form-urlencoded"} + token_response = requests.post( + token_url, + data=data, + headers=headers, + ) + access_token = token_response.json().get("access_token") + if token_response.status_code != 200: + return CustomResponse( + general_message="Failed to get access token" + ).get_failure_response() + onboard_user.delay(access_token, user_id) + return CustomResponse( + general_message="You will be added to the discord server soon" + ).get_success_response() + + +class UserInterestAPI(APIView): + permission_classes = [CustomizePermission] + + def put(self, request): + if not JWTUtils.is_jwt_authenticated(request): + return CustomResponse( + general_message="Unauthorized access" + ).get_failure_response() + user_id = JWTUtils.fetch_user_id(request) + if not (user := cache.get(f"db_user_{user_id}")): + user = User.objects.filter(id=user_id).first() + user_interest = UserInterests.objects.filter(user=user).first() + if not user_interest: + return CustomResponse( + general_message="User interests not found" + ).get_failure_response() + serializer = serializers.UserInterestSerializer( + instance=user_interest, data=request.data, context={"user": user} + ) + if serializer.is_valid(): + serializer.update(user_interest, serializer.validated_data) + return CustomResponse( + general_message="Updated interests" + ).get_success_response() + return CustomResponse(general_message=serializer.errors).get_failure_response() + + def post(self, request): + if not JWTUtils.is_jwt_authenticated(request): + return CustomResponse( + general_message="Unauthorized access" + ).get_failure_response() + user_id = JWTUtils.fetch_user_id(request) + if not (user := cache.get(f"db_user_{user_id}")): + user = User.objects.filter(id=user_id).first() + user_interest = UserInterests.objects.filter(user=user).first() + if user_interest: + return CustomResponse( + general_message="User interests already exist" + ).get_failure_response() + serializer = serializers.UserInterestSerializer( + data=request.data, context={"user": user} + ) + if serializer.is_valid(): + serializer.save() + return CustomResponse( + general_message="Added interests" + ).get_success_response() + return CustomResponse(general_message=serializer.errors).get_failure_response() class UserRegisterValidateAPI(APIView): diff --git a/api/register/serializers.py b/api/register/serializers.py index 96b978a8..9067e639 100644 --- a/api/register/serializers.py +++ b/api/register/serializers.py @@ -14,8 +14,24 @@ UserOrganizationLink, Zone, ) -from db.task import InterestGroup, Level, MucoinInviteLog, UserIgLink, UserLvlLink, Wallet -from db.user import Role, Socials, User, UserMentor, UserReferralLink, UserRoleLink, UserSettings +from db.task import ( + InterestGroup, + Level, + MucoinInviteLog, + UserIgLink, + UserLvlLink, + Wallet, +) +from db.user import ( + Role, + Socials, + User, + UserMentor, + UserReferralLink, + UserRoleLink, + UserSettings, + UserInterests, +) from utils.exception import CustomException from utils.types import OrganizationType, RoleType from utils.utils import DateTimeUtils @@ -163,7 +179,6 @@ class Meta: ] - class ReferralSerializer(serializers.ModelSerializer): user = serializers.CharField(required=False) muid = serializers.CharField(required=False) @@ -326,6 +341,7 @@ class Meta: "area_of_interest", ] + class RegisterSerializer(serializers.Serializer): user = UserSerializer() organization = UserOrgLinkSerializer(required=False) @@ -351,7 +367,7 @@ def create(self, validated_data): if mentor := validated_data.pop("mentor", None): mentor["user"] = user - MentorSerializer().create(mentor) + MentorSerializer().create(mentor) return user @@ -399,3 +415,72 @@ class Meta: def get_location(self, obj): return f"{obj.name}, {obj.zone.state.name}, {obj.zone.state.country.name}" + + +class UserInterestSerializer(serializers.ModelSerializer): + id = serializers.CharField(read_only=True) + user = serializers.CharField(read_only=True) + choosen_interests = serializers.JSONField() + other_interests = serializers.JSONField(required=False) + choosen_endgoals = serializers.JSONField() + other_endgoals = serializers.JSONField(required=False) + created_at = serializers.DateTimeField(read_only=True) + updated_at = serializers.DateTimeField(read_only=True) + + def create(self, validated_data): + validated_data["created_at"] = validated_data["updated_at"] = ( + DateTimeUtils.get_current_utc_time() + ) + if user := self.context.get("user"): + validated_data["user"] = user + else: + return serializers.ValidationError("User not found") + return super().create(validated_data) + + def update(self, instance, validated_data): + if validated_data.get("choosen_interests", None): + instance.choosen_interests = validated_data.get("choosen_interests", []) + if validated_data.get("other_interests", None): + instance.other_interests = validated_data.get("other_interests", []) + if validated_data.get("choosen_endgoals", None): + instance.choosen_endgoals = validated_data.get("choosen_endgoals", []) + if validated_data.get("other_endgoals", None): + instance.other_endgoals = validated_data.get("other_endgoals", []) + instance.updated_at = DateTimeUtils.get_current_utc_time() + return instance.save() + + def validate_choosen_interests(self, interests): + if not all( + interest in ("maker", "software", "creative", "management", "others") + for interest in interests + ): + raise serializers.ValidationError("Invalid interests selected.") + return list(set(interests)) + + def validate_choosen_endgoals(self, end_goals): + if not all( + goal + in ( + "job", + "higher_education", + "gig_work", + "entrepreneurship", + "others", + ) + for goal in end_goals + ): + raise serializers.ValidationError("Invalid end goals selected.") + return list(set(end_goals)) + + class Meta: + model = UserInterests + fields = [ + "id", + "user", + "choosen_interests", + "other_interests", + "choosen_endgoals", + "other_endgoals", + "created_at", + "updated_at", + ] diff --git a/api/register/urls.py b/api/register/urls.py index 039481f6..daf1d4ad 100644 --- a/api/register/urls.py +++ b/api/register/urls.py @@ -8,9 +8,7 @@ path("role/list/", register_views.RoleAPI.as_view()), path("colleges/", register_views.CollegesAPI.as_view()), path("department/list/", register_views.DepartmentAPI.as_view()), - path("location/", register_views.LocationSearchView.as_view()), - path("country/list/", register_views.CountryAPI.as_view()), path("state/list/", register_views.StateAPI.as_view()), path("district/list/", register_views.DistrictAPI.as_view()), @@ -20,9 +18,10 @@ path("schools/list/", register_views.SchoolAPI.as_view()), path("area-of-interest/list/", register_views.AreaOfInterestAPI.as_view()), path("lc/user-validation/", register_views.LearningCircleUserViewAPI.as_view()), - path('email-verification/', register_views.UserEmailVerificationAPI.as_view()), - path('user-country/', register_views.UserCountryAPI.as_view()), - path('user-state/', register_views.UserStateAPI.as_view()), - path('user-zone/', register_views.UserZoneAPI.as_view()), - -] \ No newline at end of file + path("email-verification/", register_views.UserEmailVerificationAPI.as_view()), + path("user-country/", register_views.UserCountryAPI.as_view()), + path("user-state/", register_views.UserStateAPI.as_view()), + path("user-zone/", register_views.UserZoneAPI.as_view()), + path("interests/", register_views.UserInterestAPI.as_view()), + path("connect-discord/", register_views.ConnectDiscordAPI.as_view()), +] diff --git a/db/user.py b/db/user.py index e40e633c..c3583ce2 100644 --- a/db/user.py +++ b/db/user.py @@ -6,6 +6,7 @@ from django.conf import settings from .managers import user_manager + # from .task import UserIgLink from decouple import config as decouple_config @@ -56,6 +57,19 @@ def save(self, *args, **kwargs): return super().save(*args, **kwargs) +class UserInterests(models.Model): + id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4) + user = models.ForeignKey(User, on_delete=models.CASCADE) + choosen_interests = models.JSONField(max_length=100) + other_interests = models.JSONField(max_length=100, blank=True, null=True) + choosen_endgoals = models.JSONField(max_length=100) + other_endgoals = models.JSONField(max_length=100, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + managed = False + db_table = 'user_interests' class UserMentor(models.Model): id = models.CharField(primary_key=True, max_length=36, default=uuid.uuid4) diff --git a/mu_celery/task.py b/mu_celery/task.py index d55c75b7..f70d2c91 100644 --- a/mu_celery/task.py +++ b/mu_celery/task.py @@ -1,7 +1,50 @@ from celery import shared_task from utils.utils import send_template_mail +import requests +from decouple import config +from db.user import User + +DISCORD_GUILD_ID = config("DISCORD_GUILD_ID") +DISCORD_BOT_TOKEN = config("DISCORD_BOT_TOKEN") @shared_task def send_email(context: dict, subject: str, address: list[str], attachment: str = None): return send_template_mail(context, subject, address, attachment) + + +@shared_task +def onboard_user(access_token: str, user_id: int): + user = User.objects.get(id=user_id) + user_response = requests.get( + "https://discord.com/api/users/@me", + headers={"Authorization": f"Bearer {access_token}"}, + ) + if user_response.status_code != 200: + return {"status": "error", "message": "Failed to get user data"} + user_data = user_response.json() + discord_user_id = user_data.get("id") + guild_url = ( + f"https://discord.com/api/guilds/{DISCORD_GUILD_ID}/members/{discord_user_id}" + ) + member_data = {"access_token": access_token} + bot_headers = { + "Authorization": f"Bot {DISCORD_BOT_TOKEN}", + "Content-Type": "application/json", + } + already_linked_account = User.objects.filter(discord_id=discord_user_id).first() + if already_linked_account: + already_linked_account.exist_in_guild = False + already_linked_account.discord_id = None + already_linked_account.save() + user.discord_id = discord_user_id + user.exist_in_guild = True + user.save() + join_response = requests.put(guild_url, json=member_data, headers=bot_headers) + if ( + join_response.status_code != 201 + and join_response.status_code != 200 + and join_response.status_code != 204 + ): + return {"status": "error", "message": "Failed to join guild"} + return {"status": "success", "message": "User onboarded successfully"}