diff --git a/lms/djangoapps/course_home_api/course_metadata/__init__.py b/lms/djangoapps/course_home_api/course_metadata/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/course_home_api/course_metadata/v1/__init__.py b/lms/djangoapps/course_home_api/course_metadata/v1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/course_home_api/course_metadata/v1/serializers.py b/lms/djangoapps/course_home_api/course_metadata/v1/serializers.py new file mode 100644 index 0000000000..ecf43ed67b --- /dev/null +++ b/lms/djangoapps/course_home_api/course_metadata/v1/serializers.py @@ -0,0 +1,37 @@ +# pylint: disable=abstract-method +""" +Course Home Course Metadata Serializers. Returns Course Metadata used for all +Course Home pages. +""" + + +from django.urls import reverse +from rest_framework import serializers + + +class CourseTabSerializer(serializers.Serializer): + """ + Serializer for the Course Home Tabs + """ + tab_id = serializers.CharField() + title = serializers.SerializerMethodField() + url = serializers.SerializerMethodField() + + def get_title(self, tab): + return tab.title or tab.get('name', '') + + def get_url(self, tab): + request = self.context.get('request') + return request.build_absolute_uri(tab.link_func(self.context.get('course'), reverse)) + + +class CourseHomeMetadataSerializer(serializers.Serializer): + """ + Serializer for the Course Home Course Metadata + """ + course_id = serializers.CharField() + is_staff = serializers.BooleanField() + number = serializers.CharField() + org = serializers.CharField() + tabs = CourseTabSerializer(many=True) + title = serializers.CharField() diff --git a/lms/djangoapps/course_home_api/course_metadata/v1/tests/__init__.py b/lms/djangoapps/course_home_api/course_metadata/v1/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/course_home_api/course_metadata/v1/tests/test_views.py b/lms/djangoapps/course_home_api/course_metadata/v1/tests/test_views.py new file mode 100644 index 0000000000..e2e6f67565 --- /dev/null +++ b/lms/djangoapps/course_home_api/course_metadata/v1/tests/test_views.py @@ -0,0 +1,53 @@ +""" +Tests for the Course Home Course Metadata API in the Course Home API +""" + + +import ddt + +from django.urls import reverse + +from course_modes.models import CourseMode +from lms.djangoapps.course_home_api.tests.utils import BaseCourseHomeTests +from student.models import CourseEnrollment +from student.tests.factories import UserFactory + + +@ddt.ddt +class CourseHomeMetadataTests(BaseCourseHomeTests): + """ + Tests for the Course Home Course Metadata API + """ + @classmethod + def setUpClass(cls): + BaseCourseHomeTests.setUpClass() + cls.url = reverse('course-home-course-metadata', args=[cls.course.id]) + + def test_get_authenticated_user(self): + CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertFalse(response.data.get('is_staff')) + # 'Course', 'Wiki', 'Progress' tabs + self.assertEqual(len(response.data.get('tabs', [])), 3) + + def test_get_authenticated_staff_user(self): + self.client.logout() + staff_user = UserFactory( + username='staff', + email='staff@example.com', + password='bar', + is_staff=True + ) + self.client.login(username=staff_user.username, password='bar') + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTrue(response.data['is_staff']) + # This differs for a staff user because they also receive the Instructor tab + # 'Course', 'Wiki', 'Progress', and 'Instructor' tabs + self.assertEqual(len(response.data.get('tabs', [])), 4) + + def test_get_unknown_course(self): + url = reverse('course-home-course-metadata', args=['course-v1:unknown+course+2T2020']) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) diff --git a/lms/djangoapps/course_home_api/course_metadata/v1/views.py b/lms/djangoapps/course_home_api/course_metadata/v1/views.py new file mode 100644 index 0000000000..315b3b458b --- /dev/null +++ b/lms/djangoapps/course_home_api/course_metadata/v1/views.py @@ -0,0 +1,65 @@ +""" +General view for the Course Home that contains metadata every page needs. +""" + + +from rest_framework.generics import RetrieveAPIView +from rest_framework.response import Response + +from opaque_keys.edx.keys import CourseKey + +from lms.djangoapps.courseware.access import has_access +from lms.djangoapps.courseware.tabs import get_course_tab_list +from lms.djangoapps.course_api.api import course_detail +from lms.djangoapps.course_home_api.course_metadata.v1.serializers import CourseHomeMetadataSerializer + + +class CourseHomeMetadataView(RetrieveAPIView): + """ + **Use Cases** + + Request Course metadata details for the Course Home MFE that every page needs. + + **Example Requests** + + GET api/course_home/v1/course_metadata/{course_key} + + **Response Values** + + Body consists of the following fields: + + course_id: (str) The Course's id (Course Run key) + is_staff: (bool) Indicates if the user is staff + number: (str) The Course's number + org: (str) The Course's organization + tabs: List of Course Tabs to display. They are serialized as: + tab_id: (str) The tab's id + title: (str) The title of the tab to display + url: (str) The url to view the tab + title: (str) The Course's display title + + **Returns** + + * 200 on success with above fields. + * 404 if the course is not available or cannot be seen. + """ + + serializer_class = CourseHomeMetadataSerializer + + def get(self, request, *args, **kwargs): + course_key_string = kwargs.get('course_key_string') + course_key = CourseKey.from_string(course_key_string) + course = course_detail(request, request.user.username, course_key) + + data = { + 'course_id': course.id, + 'is_staff': has_access(request.user, 'staff', course_key).has_access, + 'number': course.display_number_with_default, + 'org': course.display_org_with_default, + 'tabs': get_course_tab_list(request.user, course), + 'title': course.display_name_with_default, + } + context = self.get_serializer_context() + context['course'] = course + serializer = self.get_serializer_class()(data, context=context) + return Response(serializer.data) diff --git a/lms/djangoapps/course_home_api/dates/__init__.py b/lms/djangoapps/course_home_api/dates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/course_home_api/dates/v1/serializers.py b/lms/djangoapps/course_home_api/dates/v1/serializers.py index d142ca0982..1f27035d5f 100644 --- a/lms/djangoapps/course_home_api/dates/v1/serializers.py +++ b/lms/djangoapps/course_home_api/dates/v1/serializers.py @@ -1,3 +1,4 @@ +# pylint: disable=abstract-method """ Dates Tab Serializers. Represents the relevant dates for a Course. """ diff --git a/lms/djangoapps/course_home_api/dates/v1/views.py b/lms/djangoapps/course_home_api/dates/v1/views.py index 7a55fdd923..808363a1ce 100644 --- a/lms/djangoapps/course_home_api/dates/v1/views.py +++ b/lms/djangoapps/course_home_api/dates/v1/views.py @@ -56,7 +56,9 @@ class DatesTabView(RetrieveAPIView): permission_classes = (IsAuthenticated,) serializer_class = DatesTabSerializer - def get(self, request, course_key_string): + def get(self, request, *args, **kwargs): + course_key_string = kwargs.get('course_key_string') + # Enable NR tracing for this view based on course monitoring_utils.set_custom_metric('course_id', course_key_string) monitoring_utils.set_custom_metric('user_id', request.user.id) diff --git a/lms/djangoapps/course_home_api/tests/utils.py b/lms/djangoapps/course_home_api/tests/utils.py index 1fd56f4e49..cfa98b3ccb 100644 --- a/lms/djangoapps/course_home_api/tests/utils.py +++ b/lms/djangoapps/course_home_api/tests/utils.py @@ -40,7 +40,7 @@ class BaseCourseHomeTests(SharedModuleStoreTestCase): modulestore=cls.store, ) chapter = ItemFactory(parent=cls.course, category='chapter') - ItemFactory(parent=chapter, category='sequential', display_name='sequence') + ItemFactory(parent=chapter, category='sequential') CourseModeFactory(course_id=cls.course.id, mode_slug=CourseMode.AUDIT) CourseModeFactory( diff --git a/lms/djangoapps/course_home_api/urls.py b/lms/djangoapps/course_home_api/urls.py index 07753b3330..1ee86db909 100644 --- a/lms/djangoapps/course_home_api/urls.py +++ b/lms/djangoapps/course_home_api/urls.py @@ -6,15 +6,25 @@ Contains all the URLs for the Course Home from django.conf import settings from django.urls import re_path -from lms.djangoapps.course_home_api.dates.v1 import views +from lms.djangoapps.course_home_api.dates.v1.views import DatesTabView +from lms.djangoapps.course_home_api.course_metadata.v1.views import CourseHomeMetadataView urlpatterns = [] +# URL for Course metadata content +urlpatterns += [ + re_path( + r'v1/course_metadata/{}'.format(settings.COURSE_KEY_PATTERN), + CourseHomeMetadataView.as_view(), + name='course-home-course-metadata' + ), +] + # Dates Tab URLs urlpatterns += [ re_path( r'v1/dates/{}'.format(settings.COURSE_KEY_PATTERN), - views.DatesTabView.as_view(), + DatesTabView.as_view(), name='course-home-dates-tab' ), ]