diff --git a/api/common/common_views.py b/api/common/common_views.py index 5bf3f6f59..0b3582f40 100644 --- a/api/common/common_views.py +++ b/api/common/common_views.py @@ -734,7 +734,8 @@ class ListIGAPI(APIView): })}, ) def get(self, request): - return CustomResponse(response=InterestGroup.objects.all().values("name")).get_success_response() + from utils.types import InterestGroupStatus + return CustomResponse(response=InterestGroup.objects.filter(status=InterestGroupStatus.ACTIVE.value).values("name")).get_success_response() class IGDetailAPI(APIView): @@ -745,10 +746,11 @@ class IGDetailAPI(APIView): ) def get(self, request, pk): from api.dashboard.ig.dash_ig_serializer import InterestGroupSerializer + from utils.types import InterestGroupStatus ig_data = ( InterestGroup.objects.prefetch_related("user_ig_link_ig") - .filter(id=pk) + .filter(id=pk, status=InterestGroupStatus.ACTIVE.value) .first() ) diff --git a/api/dashboard/ig/dash_ig_view.py b/api/dashboard/ig/dash_ig_view.py index 5ee15666a..d18075551 100644 --- a/api/dashboard/ig/dash_ig_view.py +++ b/api/dashboard/ig/dash_ig_view.py @@ -878,8 +878,9 @@ class InterestGroupListApi(APIView): responses={200: InterestGroupSerializer}, ) def get(self, request): + from utils.types import InterestGroupStatus ig = ( - InterestGroup.objects.all() + InterestGroup.objects.filter(status=InterestGroupStatus.ACTIVE.value) .select_related("created_by", "updated_by") .prefetch_related("user_ig_link_ig") .annotate(members=Count("user_ig_link_ig")) diff --git a/api/dashboard/learningcircle/learningcircle_views.py b/api/dashboard/learningcircle/learningcircle_views.py index 1343656c6..86afbd319 100644 --- a/api/dashboard/learningcircle/learningcircle_views.py +++ b/api/dashboard/learningcircle/learningcircle_views.py @@ -1440,10 +1440,19 @@ def get(self, request): @extend_schema(tags=['Dashboard - Learningcircle'], description="Create Circle Invite Status.", responses={200: CircleInviteStatusSerializer}, ) - def post(self, request, link_id: str): + def post(self, request, link_id: str = None): """Accept or reject an invitation by its link_id.""" user_id = JWTUtils.fetch_user_id(request) + if link_id is None: + link_id = request.data.get('link_id') + + if not link_id: + return CustomResponse( + general_message="link_id is required" + ).get_failure_response() + + try: link = UserCircleLink.objects.get(id=link_id, user_id=user_id, is_invited=True) except UserCircleLink.DoesNotExist: diff --git a/api/dashboard/learningcircle/tests/__init__.py b/api/dashboard/learningcircle/tests/__init__.py new file mode 100644 index 000000000..a6131c10e --- /dev/null +++ b/api/dashboard/learningcircle/tests/__init__.py @@ -0,0 +1 @@ +# init diff --git a/api/dashboard/learningcircle/tests/test_learningcircle.py b/api/dashboard/learningcircle/tests/test_learningcircle.py new file mode 100644 index 000000000..8fedc9d77 --- /dev/null +++ b/api/dashboard/learningcircle/tests/test_learningcircle.py @@ -0,0 +1,160 @@ +import pytest +from datetime import timedelta +import jwt +from django.conf import settings +from rest_framework.test import APIClient +from db.user import User +from db.task import InterestGroup +from db.learning_circle import LearningCircle, UserCircleLink +from utils.utils import DateTimeUtils + +def get_auth_token(user_id): + expiry_dt = DateTimeUtils.get_current_utc_time() + timedelta(days=1) + expiry_str = expiry_dt.strftime("%Y-%m-%d %H:%M:%S%z") + payload = { + "id": user_id, + "expiry": expiry_str, + "roles": ["Student"] + } + token = jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") + return f"Bearer {token}" + +@pytest.fixture +def user_fixture(db): + return User.objects.create( + id="user-1", + muid="USER1@mulearn", + full_name="Invited User", + email="invited@test.com", + ) + +@pytest.fixture +def inviter_fixture(db): + return User.objects.create( + id="user-2", + muid="USER2@mulearn", + full_name="Inviter User", + email="inviter@test.com", + ) + +@pytest.fixture +def ig_fixture(db, inviter_fixture): + return InterestGroup.objects.create( + id="ig-1", + name="Python", + code="PY", + icon="py", + created_by=inviter_fixture, + updated_by=inviter_fixture, + ) + +@pytest.fixture +def circle_fixture(db, ig_fixture, inviter_fixture): + return LearningCircle.objects.create( + id="circle-1", + ig=ig_fixture, + title="Python Circle", + created_by=inviter_fixture, + ) + +@pytest.fixture +def invite_link_fixture(db, user_fixture, circle_fixture, inviter_fixture): + return UserCircleLink.objects.create( + id="link-1234", + user=user_fixture, + circle=circle_fixture, + lead=False, + is_invited=True, + invited_by=inviter_fixture, + accepted=None, + ) + +@pytest.fixture +def auth_client(user_fixture): + client = APIClient() + token = get_auth_token(user_fixture.id) + client.credentials(HTTP_AUTHORIZATION=token) + return client + +@pytest.mark.django_db +def test_accept_invite_via_body(auth_client, invite_link_fixture): + url = "/api/v1/dashboard/learningcircle/invite/status/" + payload = { + "link_id": invite_link_fixture.id, + "action": "accept" + } + response = auth_client.post(url, payload, format="json") + assert response.status_code == 200 + assert response.json()["message"]["general"][0] == "Invitation accepted successfully" + + # Verify DB update + invite_link_fixture.refresh_from_db() + assert invite_link_fixture.accepted is True + assert invite_link_fixture.accepted_at is not None + +@pytest.mark.django_db +def test_reject_invite_via_body(auth_client, invite_link_fixture): + url = "/api/v1/dashboard/learningcircle/invite/status/" + payload = { + "link_id": invite_link_fixture.id, + "action": "reject" + } + response = auth_client.post(url, payload, format="json") + assert response.status_code == 200 + assert response.json()["message"]["general"][0] == "Invitation rejected successfully" + + # Verify DB update + invite_link_fixture.refresh_from_db() + assert invite_link_fixture.accepted is False + assert invite_link_fixture.accepted_at is None + +@pytest.mark.django_db +def test_accept_invite_via_url(auth_client, invite_link_fixture): + url = f"/api/v1/dashboard/learningcircle/invite/status/{invite_link_fixture.id}/" + payload = { + "action": "accept" + } + response = auth_client.post(url, payload, format="json") + assert response.status_code == 200 + assert response.json()["message"]["general"][0] == "Invitation accepted successfully" + + # Verify DB update + invite_link_fixture.refresh_from_db() + assert invite_link_fixture.accepted is True + assert invite_link_fixture.accepted_at is not None + +@pytest.mark.django_db +def test_reject_invite_via_url(auth_client, invite_link_fixture): + url = f"/api/v1/dashboard/learningcircle/invite/status/{invite_link_fixture.id}/" + payload = { + "action": "reject" + } + response = auth_client.post(url, payload, format="json") + assert response.status_code == 200 + assert response.json()["message"]["general"][0] == "Invitation rejected successfully" + + # Verify DB update + invite_link_fixture.refresh_from_db() + assert invite_link_fixture.accepted is False + assert invite_link_fixture.accepted_at is None + +@pytest.mark.django_db +def test_invite_status_missing_link_id(auth_client): + url = "/api/v1/dashboard/learningcircle/invite/status/" + payload = { + "action": "accept" + } + response = auth_client.post(url, payload, format="json") + assert response.status_code == 400 + assert response.json()["message"]["general"][0] == "link_id is required" + +@pytest.mark.django_db +def test_invite_status_invalid_link_id(auth_client): + url = "/api/v1/dashboard/learningcircle/invite/status/" + payload = { + "link_id": "non-existent-link-id", + "action": "accept" + } + response = auth_client.post(url, payload, format="json") + assert response.status_code == 400 + assert response.json()["message"]["general"][0] == "Invitation not found" diff --git a/api/dashboard/projects/projects_serializer.py b/api/dashboard/projects/projects_serializer.py index 0c866d5ad..12c6f44d3 100644 --- a/api/dashboard/projects/projects_serializer.py +++ b/api/dashboard/projects/projects_serializer.py @@ -122,6 +122,7 @@ class ProjectSerializer(serializers.ModelSerializer): updated_by = serializers.CharField(source="updated_by.full_name", read_only=True) created_by = serializers.CharField(source="created_by.full_name", read_only=True) created_by_id = serializers.CharField(source="created_by.id", read_only=True) + created_by_muid = serializers.CharField(source="created_by.muid", read_only=True) logo = serializers.ImageField(max_length=None, use_url=True, required=False, allow_null=True) images = ProjectImageSerializer(many=True, read_only=True) links = ProjectLinkSerializer(many=True, read_only=True) @@ -136,7 +137,7 @@ class Meta: "id", "title", "description", "status", "logo", "images", "links", "skills", "members", "votes", "comments", - "created_by", "created_by_id", "updated_by", + "created_by", "created_by_id", "created_by_muid", "updated_by", "created_at", "updated_at", ] diff --git a/utils/types.py b/utils/types.py index 48b850b26..90bedbb78 100644 --- a/utils/types.py +++ b/utils/types.py @@ -104,6 +104,13 @@ class IntegrationType(Enum): KKEM = "DWMS" +class InterestGroupStatus(Enum): + ACTIVE = "active" + REQUESTED = "requested" + CANCELLED = "cancelled" + REJECTED = "rejected" + + class TasksTypesHashtag(Enum): REFERRAL = "referral" MUCOIN = "mucoin"