From 6c7da60cb5bf0a2ab7389839962cfe6fb5b19600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Paccoud=20-=20France=20Universit=C3=A9=20Num?= =?UTF-8?q?=C3=A9rique?= Date: Fri, 7 Jul 2023 15:16:13 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(api)=20allow=20retrieving=20courses?= =?UTF-8?q?=20by=20their=20code=20as=20well=20as=20their=20pk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Richie knows the code but not the ID of courses. --- src/backend/joanie/core/api/client.py | 36 ++++++++++++++----- src/backend/joanie/core/factories.py | 2 +- .../joanie/tests/core/test_api_course.py | 31 ++++++++++++++++ 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/backend/joanie/core/api/client.py b/src/backend/joanie/core/api/client.py index bb082fd682..7f5b76c0b3 100644 --- a/src/backend/joanie/core/api/client.py +++ b/src/backend/joanie/core/api/client.py @@ -1,6 +1,8 @@ """ Client API endpoints """ +import uuid + from django.db import IntegrityError, transaction from django.db.models import Count, OuterRef, Q, Subquery from django.http import HttpResponse @@ -12,6 +14,7 @@ from rest_framework.decorators import action from rest_framework.exceptions import NotFound from rest_framework.exceptions import ValidationError as DRFValidationError +from rest_framework.generics import get_object_or_404 from rest_framework.response import Response from joanie.core import enums, filters, models, permissions, serializers @@ -629,24 +632,24 @@ class CourseAccessViewSet( """ API ViewSet for all interactions with course accesses. - GET /api/courses//accesses/: + GET /api/courses//accesses/: Return list of all course accesses related to the logged-in user or one course access if an id is provided. - POST /api/courses//accesses/ with expected data: + POST /api/courses//accesses/ with expected data: - user: str - role: str [owner|admin|member] Return newly created course access - PUT /api/courses//accesses// with expected data: + PUT /api/courses//accesses// with expected data: - role: str [owner|admin|member] Return updated course access - PATCH /api/courses//accesses// with expected data: + PATCH /api/courses//accesses// with expected data: - role: str [owner|admin|member] Return partially updated course access - DELETE /api/courses//accesses// + DELETE /api/courses//accesses// Delete targeted course access """ @@ -702,16 +705,16 @@ class CourseViewSet( GET /api/courses/ Return list of all courses related to the logged-in user. - GET /api/courses/: + GET /api/courses/: Return one course if an id is provided. - GET /api/courses/:/wish + GET /api/courses/:/wish Return wish status on this course for the authenticated user - POST /api/courses/:/wish + POST /api/courses/:/wish Confirm a wish on this course for the authenticated user - DELETE /api/courses/:/wish + DELETE /api/courses/:/wish Delete any existing wish on this course for the authenticated user """ @@ -723,6 +726,21 @@ class CourseViewSet( serializer_class = serializers.CourseSerializer ordering = ["-created_on"] + def get_object(self): + """Allow getting a course by its pk or by its code.""" + queryset = self.filter_queryset(self.get_queryset()) + try: + uuid.UUID(self.kwargs["pk"]) + except ValueError: + filter_field = "code__iexact" + else: + filter_field = "pk" + + obj = get_object_or_404(queryset, **{filter_field: self.kwargs["pk"]}) + # May raise a permission denied + self.check_object_permissions(self.request, obj) + return obj + def get_queryset(self): """ Custom queryset to get user courses diff --git a/src/backend/joanie/core/factories.py b/src/backend/joanie/core/factories.py index 89a2dc8a83..768074a759 100644 --- a/src/backend/joanie/core/factories.py +++ b/src/backend/joanie/core/factories.py @@ -116,7 +116,7 @@ class CourseFactory(factory.django.DjangoModelFactory): class Meta: model = models.Course - code = factory.Sequence(lambda n: n) + code = factory.Sequence(lambda n: f"{n:06d}") title = factory.Sequence(lambda n: f"Course {n}") cover = factory.django.ImageField( filename="cover.png", format="png", width=1, height=1 diff --git a/src/backend/joanie/tests/core/test_api_course.py b/src/backend/joanie/tests/core/test_api_course.py index b9de3fa1f0..0521cc542f 100644 --- a/src/backend/joanie/tests/core/test_api_course.py +++ b/src/backend/joanie/tests/core/test_api_course.py @@ -261,6 +261,37 @@ def test_api_course_get_authenticated_with_access(self, _): }, ) + @mock.patch.object( + fields.ThumbnailDetailField, + "to_representation", + return_value="_this_field_is_mocked", + ) + def test_api_course_get_authenticated_by_code(self, _): + """ + Authenticated users should be able to get a course through its code + if they have access to it. + """ + user = factories.UserFactory() + token = self.get_user_token(user.username) + + course = factories.CourseFactory(code="MYCODE-0088") + factories.UserCourseAccessFactory(user=user, course=course) + factories.CourseProductRelationFactory( + course=course, + product=factories.ProductFactory(), + organizations=[factories.OrganizationFactory()], + ) + + with self.assertNumQueries(10): + response = self.client.get( + "/api/v1.0/courses/mycode-0088/", + HTTP_AUTHORIZATION=f"Bearer {token}", + ) + + self.assertEqual(response.status_code, 200) + content = response.json() + self.assertEqual(content["id"], str(course.id)) + def test_api_course_create_anonymous(self): """ Anonymous users should not be able to create a course.