Skip to content

Commit 06b0584

Browse files
committed
✨(api) allow retrieving courses by their code as well as their pk
Richie knows the code but not the ID of courses.
1 parent 71a58e4 commit 06b0584

3 files changed

Lines changed: 59 additions & 10 deletions

File tree

src/backend/joanie/core/api/client.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
Client API endpoints
33
"""
4+
import uuid
5+
46
from django.db import IntegrityError, transaction
57
from django.db.models import Count, OuterRef, Q, Subquery
68
from django.http import HttpResponse
@@ -12,6 +14,7 @@
1214
from rest_framework.decorators import action
1315
from rest_framework.exceptions import NotFound
1416
from rest_framework.exceptions import ValidationError as DRFValidationError
17+
from rest_framework.generics import get_object_or_404
1518
from rest_framework.response import Response
1619

1720
from joanie.core import enums, filters, models, permissions, serializers
@@ -629,24 +632,24 @@ class CourseAccessViewSet(
629632
"""
630633
API ViewSet for all interactions with course accesses.
631634
632-
GET /api/courses/<course_id>/accesses/:<course_access_id>
635+
GET /api/courses/<course_id|course_code>/accesses/:<course_access_id>
633636
Return list of all course accesses related to the logged-in user or one
634637
course access if an id is provided.
635638
636-
POST /api/courses/<course_id>/accesses/ with expected data:
639+
POST /api/courses/<course_id|course_code>/accesses/ with expected data:
637640
- user: str
638641
- role: str [owner|admin|member]
639642
Return newly created course access
640643
641-
PUT /api/courses/<course_id>/accesses/<course_access_id>/ with expected data:
644+
PUT /api/courses/<course_id|course_code>/accesses/<course_access_id>/ with expected data:
642645
- role: str [owner|admin|member]
643646
Return updated course access
644647
645-
PATCH /api/courses/<course_id>/accesses/<course_access_id>/ with expected data:
648+
PATCH /api/courses/<course_id|course_code>/accesses/<course_access_id>/ with expected data:
646649
- role: str [owner|admin|member]
647650
Return partially updated course access
648651
649-
DELETE /api/courses/<course_id>/accesses/<course_access_id>/
652+
DELETE /api/courses/<course_id|course_code>/accesses/<course_access_id>/
650653
Delete targeted course access
651654
"""
652655

@@ -702,16 +705,16 @@ class CourseViewSet(
702705
GET /api/courses/
703706
Return list of all courses related to the logged-in user.
704707
705-
GET /api/courses/:<course_id>
708+
GET /api/courses/:<course_id|course_code>
706709
Return one course if an id is provided.
707710
708-
GET /api/courses/:<course_id>/wish
711+
GET /api/courses/:<course_id|course_code>/wish
709712
Return wish status on this course for the authenticated user
710713
711-
POST /api/courses/:<course_id>/wish
714+
POST /api/courses/:<course_id|course_code>/wish
712715
Confirm a wish on this course for the authenticated user
713716
714-
DELETE /api/courses/:<course_id>/wish
717+
DELETE /api/courses/:<course_id|course_code>/wish
715718
Delete any existing wish on this course for the authenticated user
716719
"""
717720

@@ -723,6 +726,21 @@ class CourseViewSet(
723726
serializer_class = serializers.CourseSerializer
724727
ordering = ["-created_on"]
725728

729+
def get_object(self):
730+
"""Allow getting a course by its pk or by its code."""
731+
queryset = self.filter_queryset(self.get_queryset())
732+
try:
733+
uuid.UUID(self.kwargs["pk"])
734+
except ValueError:
735+
filter_field = "code__iexact"
736+
else:
737+
filter_field = "pk"
738+
739+
obj = get_object_or_404(queryset, **{filter_field: self.kwargs["pk"]})
740+
# May raise a permission denied
741+
self.check_object_permissions(self.request, obj)
742+
return obj
743+
726744
def get_queryset(self):
727745
"""
728746
Custom queryset to get user courses

src/backend/joanie/core/factories.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ class CourseFactory(factory.django.DjangoModelFactory):
116116
class Meta:
117117
model = models.Course
118118

119-
code = factory.Sequence(lambda n: n)
119+
code = factory.Sequence(lambda n: f"{n:06d}")
120120
title = factory.Sequence(lambda n: f"Course {n}")
121121
cover = factory.django.ImageField(
122122
filename="cover.png", format="png", width=1, height=1

src/backend/joanie/tests/core/test_api_course.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,37 @@ def test_api_course_get_authenticated_with_access(self, _):
261261
},
262262
)
263263

264+
@mock.patch.object(
265+
fields.ThumbnailDetailField,
266+
"to_representation",
267+
return_value="_this_field_is_mocked",
268+
)
269+
def test_api_course_get_authenticated_by_code(self, _):
270+
"""
271+
Authenticated users should be able to get a course through its code
272+
if they have access to it.
273+
"""
274+
user = factories.UserFactory()
275+
token = self.get_user_token(user.username)
276+
277+
course = factories.CourseFactory(code="MYCODE-0088")
278+
factories.UserCourseAccessFactory(user=user, course=course)
279+
factories.CourseProductRelationFactory(
280+
course=course,
281+
product=factories.ProductFactory(),
282+
organizations=[factories.OrganizationFactory()],
283+
)
284+
285+
with self.assertNumQueries(8):
286+
response = self.client.get(
287+
"/api/v1.0/courses/mycode-0088/",
288+
HTTP_AUTHORIZATION=f"Bearer {token}",
289+
)
290+
291+
self.assertEqual(response.status_code, 200)
292+
content = response.json()
293+
self.assertEqual(content["id"], str(course.id))
294+
264295
def test_api_course_create_anonymous(self):
265296
"""
266297
Anonymous users should not be able to create a course.

0 commit comments

Comments
 (0)