From 8a74bbd5fb46d5e6920931e4378e79ca8a986ac6 Mon Sep 17 00:00:00 2001 From: Dillon Dumesnil Date: Thu, 21 May 2020 08:37:17 -0700 Subject: [PATCH] AA-150: Adding a general end point for the Course Home MFE This endpoint is intended to contain generic information for every page in the Course Home MFE. It contains course information for the header (Course title, org, number, key) as well as staff status for the user and the Course tabs to display. --- .../course_metadata/__init__.py | 0 .../course_metadata/v1/__init__.py | 0 .../course_metadata/v1/serializers.py | 37 +++++++++++ .../course_metadata/v1/tests/__init__.py | 0 .../course_metadata/v1/tests/test_views.py | 53 +++++++++++++++ .../course_metadata/v1/views.py | 65 +++++++++++++++++++ .../course_home_api/dates/__init__.py | 0 .../course_home_api/dates/v1/serializers.py | 1 + .../course_home_api/dates/v1/views.py | 4 +- lms/djangoapps/course_home_api/tests/utils.py | 2 +- lms/djangoapps/course_home_api/urls.py | 14 +++- 11 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 lms/djangoapps/course_home_api/course_metadata/__init__.py create mode 100644 lms/djangoapps/course_home_api/course_metadata/v1/__init__.py create mode 100644 lms/djangoapps/course_home_api/course_metadata/v1/serializers.py create mode 100644 lms/djangoapps/course_home_api/course_metadata/v1/tests/__init__.py create mode 100644 lms/djangoapps/course_home_api/course_metadata/v1/tests/test_views.py create mode 100644 lms/djangoapps/course_home_api/course_metadata/v1/views.py create mode 100644 lms/djangoapps/course_home_api/dates/__init__.py 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' ), ]