diff --git a/lms/djangoapps/course_home_api/course_metadata/v1/serializers.py b/lms/djangoapps/course_home_api/course_metadata/v1/serializers.py index ecf43ed67b6954a8daeb4ccef93560e4d3471ca9..e491d8ad75e813b02e67758e59af8494c02ae435 100644 --- a/lms/djangoapps/course_home_api/course_metadata/v1/serializers.py +++ b/lms/djangoapps/course_home_api/course_metadata/v1/serializers.py @@ -35,3 +35,4 @@ class CourseHomeMetadataSerializer(serializers.Serializer): org = serializers.CharField() tabs = CourseTabSerializer(many=True) title = serializers.CharField() + is_self_paced = serializers.BooleanField() diff --git a/lms/djangoapps/course_home_api/course_metadata/v1/views.py b/lms/djangoapps/course_home_api/course_metadata/v1/views.py index 315b3b458bc8052cc7c9be0eda0bc15808231de1..ce50fed96438f46af0077b5ba8b0303cf3f5c89a 100644 --- a/lms/djangoapps/course_home_api/course_metadata/v1/views.py +++ b/lms/djangoapps/course_home_api/course_metadata/v1/views.py @@ -50,7 +50,6 @@ class CourseHomeMetadataView(RetrieveAPIView): 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, @@ -58,6 +57,7 @@ class CourseHomeMetadataView(RetrieveAPIView): 'org': course.display_org_with_default, 'tabs': get_course_tab_list(request.user, course), 'title': course.display_name_with_default, + 'is_self_paced': getattr(course, 'self_paced', False), } context = self.get_serializer_context() context['course'] = course diff --git a/lms/djangoapps/course_home_api/dates/v1/serializers.py b/lms/djangoapps/course_home_api/dates/v1/serializers.py index bfcfcfdc902db4bcd9b576787d074ba085d2ecf2..dd72d2193d0b5cdd33c144697d3a7748e30269b5 100644 --- a/lms/djangoapps/course_home_api/dates/v1/serializers.py +++ b/lms/djangoapps/course_home_api/dates/v1/serializers.py @@ -7,6 +7,7 @@ Dates Tab Serializers. Represents the relevant dates for a Course. from rest_framework import serializers from lms.djangoapps.courseware.date_summary import VerificationDeadlineDate +from lms.djangoapps.course_home_api.mixins import DatesBannerSerializerMixin class DateSummarySerializer(serializers.Serializer): @@ -33,7 +34,7 @@ class DateSummarySerializer(serializers.Serializer): return '' -class DatesTabSerializer(serializers.Serializer): +class DatesTabSerializer(DatesBannerSerializerMixin, serializers.Serializer): """ Serializer for the Dates Tab """ diff --git a/lms/djangoapps/course_home_api/dates/v1/tests/test_views.py b/lms/djangoapps/course_home_api/dates/v1/tests/test_views.py index e230542d035295667efcd1846faa41387c3a60f9..6de10fbc583ae0f491686857405c4a9ac7ebf268 100644 --- a/lms/djangoapps/course_home_api/dates/v1/tests/test_views.py +++ b/lms/djangoapps/course_home_api/dates/v1/tests/test_views.py @@ -56,3 +56,13 @@ class DatesTabTestViews(BaseCourseHomeTests): url = reverse('course-home-dates-tab', args=['course-v1:unknown+course+2T2020']) response = self.client.get(url) self.assertEqual(response.status_code, 404) + + @COURSE_HOME_MICROFRONTEND.override(active=True) + @COURSE_HOME_MICROFRONTEND_DATES_TAB.override(active=True) + def test_banner_data_is_returned(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'missed_deadlines') + self.assertContains(response, 'missed_gated_content') + self.assertContains(response, 'content_type_gating_enabled') + self.assertContains(response, 'verified_upgrade_link') diff --git a/lms/djangoapps/course_home_api/mixins.py b/lms/djangoapps/course_home_api/mixins.py new file mode 100644 index 0000000000000000000000000000000000000000..ab3111fd14459a5328e982b98bebb5a34d61899d --- /dev/null +++ b/lms/djangoapps/course_home_api/mixins.py @@ -0,0 +1,45 @@ +# pylint: disable=abstract-method +""" +Course Home Mixins. +""" + + +from rest_framework import serializers + +from opaque_keys.edx.keys import CourseKey + +from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link +from openedx.features.content_type_gating.models import ContentTypeGatingConfig +from openedx.features.course_experience.utils import dates_banner_should_display + + +class DatesBannerSerializerMixin(serializers.Serializer): + """ + Serializer Mixin for displaying the dates banner. + Can be added to any serializer who's tab wants to display it. + """ + dates_banner_info = serializers.SerializerMethodField() + + def get_dates_banner_info(self, _): + """ + Serializer mixin for returning date banner info. Gets its input from + the views course_key_string url parameter and the request's user object. + """ + info = { + 'missed_deadlines': False, + 'content_type_gating_enabled': False, + } + course_key_string = self.context['view'].kwargs.get('course_key_string') + if course_key_string: + course_key = CourseKey.from_string(course_key_string) + request = self.context['request'] + missed_deadlines, missed_gated_content = dates_banner_should_display(course_key, request.user) + info['missed_deadlines'] = missed_deadlines + info['missed_gated_content'] = missed_gated_content + info['content_type_gating_enabled'] = ContentTypeGatingConfig.enabled_for_enrollment( + user=request.user, + course_key=course_key, + ) + if info['content_type_gating_enabled']: + info['verified_upgrade_link'] = verified_upgrade_deadline_link(request.user, course_id=course_key) + return info diff --git a/lms/urls.py b/lms/urls.py index 3cca394aae57655ced9a0fcfa1891eb68cff16aa..33248fafb0de3e9af9058fb9e0926d77ab245cbd 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -963,3 +963,8 @@ urlpatterns.extend(plugin_urls.get_patterns(plugin_constants.ProjectType.LMS)) urlpatterns += [ url(r'^api/course_home/', include('lms.djangoapps.course_home_api.urls')), ] + +# Course Experience API urls +urlpatterns += [ + url(r'^api/course_experience/', include('openedx.features.course_experience.api.v1.urls')), +] diff --git a/openedx/features/course_experience/api/__init__.py b/openedx/features/course_experience/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/openedx/features/course_experience/api/v1/__init__.py b/openedx/features/course_experience/api/v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/openedx/features/course_experience/api/v1/tests/test_views.py b/openedx/features/course_experience/api/v1/tests/test_views.py new file mode 100644 index 0000000000000000000000000000000000000000..adcf340aa01521dbacb57efaba504f979bd1774f --- /dev/null +++ b/openedx/features/course_experience/api/v1/tests/test_views.py @@ -0,0 +1,31 @@ +""" +Tests for reset deadlines endpoint. +""" +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 + + +@ddt.ddt +class ResetCourseDeadlinesViewTests(BaseCourseHomeTests): + """ + Tests for reset deadlines endpoint. + """ + @ddt.data(CourseMode.VERIFIED) + def test_reset_deadlines(self, enrollment_mode): + CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode) + # Test correct post body + response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id}) + self.assertEqual(response.status_code, 200) + # Test body with incorrect body param + response = self.client.post(reverse('course-experience-reset-course-deadlines'), {'course': self.course.id}) + self.assertEqual(response.status_code, 400) + # Test body with additional incorrect body param + response = self.client.post( + reverse('course-experience-reset-course-deadlines'), {'course_key': self.course.id, 'invalid': 'value'} + ) + self.assertEqual(response.status_code, 400) diff --git a/openedx/features/course_experience/api/v1/urls.py b/openedx/features/course_experience/api/v1/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..66ca7c43c2ce7e70969d8543a6d46557de188fae --- /dev/null +++ b/openedx/features/course_experience/api/v1/urls.py @@ -0,0 +1,19 @@ +""" +Contains URLs for the Course Experience API +""" + + +from django.urls import re_path + +from openedx.features.course_experience.api.v1.views import reset_course_deadlines + +urlpatterns = [] + +# URL for resetting course deadlines +urlpatterns += [ + re_path( + r'v1/reset_course_deadlines', + reset_course_deadlines, + name='course-experience-reset-course-deadlines' + ), +] diff --git a/openedx/features/course_experience/api/v1/views.py b/openedx/features/course_experience/api/v1/views.py new file mode 100644 index 0000000000000000000000000000000000000000..52871acdd4fca25971ba3b42a6ce27cae20c1146 --- /dev/null +++ b/openedx/features/course_experience/api/v1/views.py @@ -0,0 +1,32 @@ +from rest_framework.decorators import api_view, permission_classes +from rest_framework.exceptions import APIException, ParseError +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from openedx.core.djangoapps.schedules.utils import reset_self_paced_schedule + + +class UnableToResetDeadlines(APIException): + status_code = 400 + default_detail = 'Unable to reset deadlines.' + default_code = 'unable_to_reset_deadlines' + + +@permission_classes((IsAuthenticated,)) +@api_view(['POST']) +def reset_course_deadlines(request): + course_key = request.data.get('course_key', None) + + # If body doesnt contain 'course_key', return 400 to client. + if not course_key: + raise ParseError("'course_key' is required.") + + # If body contains params other than 'course_key', return 400 to client. + if len(request.data) > 1: + raise ParseError("Only 'course_key' is expected.") + + try: + reset_self_paced_schedule(request.user, course_key) + return Response({'message': 'Deadlines successfully reset.'}) + except Exception: + raise UnableToResetDeadlines