diff --git a/openedx/core/djangoapps/courseware_api/tests/test_views.py b/openedx/core/djangoapps/courseware_api/tests/test_views.py index 19bf77f26d1a5503c9959729026490dc698aab17..5379464f593bb2642184aef449939fc696cd969d 100644 --- a/openedx/core/djangoapps/courseware_api/tests/test_views.py +++ b/openedx/core/djangoapps/courseware_api/tests/test_views.py @@ -6,6 +6,7 @@ from datetime import datetime import ddt import mock +from completion.test_utils import CompletionWaffleTestMixin, submit_completions_for_testing from django.conf import settings from lms.djangoapps.courseware.access_utils import ACCESS_DENIED, ACCESS_GRANTED @@ -35,6 +36,9 @@ class BaseCoursewareTests(SharedModuleStoreTestCase): emit_signals=True, modulestore=cls.store, ) + cls.chapter = ItemFactory(parent=cls.course, category='chapter') + cls.sequence = ItemFactory(parent=cls.chapter, category='sequential', display_name='sequence') + cls.unit = ItemFactory.create(parent=cls.sequence, category='vertical', display_name="Vertical") cls.user = UserFactory( username='student', @@ -114,9 +118,6 @@ class SequenceApiTestViews(BaseCoursewareTests): @classmethod def setUpClass(cls): super().setUpClass() - chapter = ItemFactory(parent=cls.course, category='chapter') - cls.sequence = ItemFactory(parent=chapter, category='sequential', display_name='sequence') - ItemFactory.create(parent=cls.sequence, category='vertical', display_name="Vertical") cls.url = '/api/courseware/sequence/{}'.format(cls.sequence.location) @classmethod @@ -125,9 +126,33 @@ class SequenceApiTestViews(BaseCoursewareTests): super().tearDownClass() def test_sequence_metadata(self): - print(self.url) - print(self.course.location) response = self.client.get(self.url) assert response.status_code == 200 assert response.data['display_name'] == 'sequence' assert len(response.data['items']) == 1 + + +class ResumeApiTestViews(BaseCoursewareTests, CompletionWaffleTestMixin): + """ + Tests for the resume API + """ + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.url = '/api/courseware/resume/{}'.format(cls.course.id) + + def test_resume_no_completion(self): + response = self.client.get(self.url) + assert response.status_code == 200 + assert response.data['block_id'] is None + assert response.data['unit_id'] is None + assert response.data['section_id'] is None + + def test_resume_with_completion(self): + self.override_waffle_switch(True) + submit_completions_for_testing(self.user, [self.unit.location]) + response = self.client.get(self.url) + assert response.status_code == 200 + assert response.data['block_id'] == str(self.unit.location) + assert response.data['unit_id'] == str(self.unit.location) + assert response.data['section_id'] == str(self.sequence.location) diff --git a/openedx/core/djangoapps/courseware_api/urls.py b/openedx/core/djangoapps/courseware_api/urls.py index 865955a6f1f72c929a9d94fc0afb07700b1b26a4..fd2c9fd0310e88069c7fe6c0a43b72fb3bc0af66 100644 --- a/openedx/core/djangoapps/courseware_api/urls.py +++ b/openedx/core/djangoapps/courseware_api/urls.py @@ -15,4 +15,7 @@ urlpatterns = [ url(r'^sequence/{}'.format(settings.USAGE_KEY_PATTERN), views.SequenceMetadata.as_view(), name="sequence-api"), + url(r'^resume/{}'.format(settings.COURSE_KEY_PATTERN), + views.Resume.as_view(), + name="resume-api"), ] diff --git a/openedx/core/djangoapps/courseware_api/views.py b/openedx/core/djangoapps/courseware_api/views.py index 52f26dffcae7ad98115d681dbe8ca362700086b0..d2cde8cccd407f6eff7b4b79af9ccb73ff68286b 100644 --- a/openedx/core/djangoapps/courseware_api/views.py +++ b/openedx/core/djangoapps/courseware_api/views.py @@ -5,16 +5,19 @@ Course API Views import json from babel.numbers import get_currency_symbol +from completion.exceptions import UnavailableCompletionData +from completion.utilities import get_key_to_last_completed_block from django.urls import reverse +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication +from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser from opaque_keys.edx.keys import CourseKey, UsageKey from rest_framework.generics import RetrieveAPIView +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from course_modes.models import CourseMode from edxnotes.helpers import is_feature_enabled -from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication -from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser from lms.djangoapps.course_api.api import course_detail from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.courses import check_course_access @@ -26,6 +29,8 @@ from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.access import generate_course_expired_message from openedx.features.discounts.utils import generate_offer_html from student.models import CourseEnrollment +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.search import path_to_location from .serializers import CourseInfoSerializer @@ -268,3 +273,61 @@ class SequenceMetadata(DeveloperErrorViewMixin, APIView): str(usage_key), disable_staff_debug_info=True) return Response(json.loads(sequence.handle_ajax('metadata', None))) + + +class Resume(DeveloperErrorViewMixin, APIView): + """ + **Use Cases** + + Request the last completed block in a course + + **Example Requests** + + GET /api/courseware/resume/{course_key} + + **Response Values** + + Body consists of the following fields: + + * block: the last completed block key + * section: the key to the section + * unit: the key to the unit + If no completion data is available, the keys will be null + + **Returns** + + * 200 on success with above fields. + * 400 if an invalid parameter was sent. + * 403 if a user who does not have permission to masquerade as + another user specifies a username other than their own. + * 404 if the course is not available or cannot be seen. + """ + + authentication_classes = ( + JwtAuthentication, + SessionAuthenticationAllowInactiveUser, + ) + permission_classes = (IsAuthenticated, ) + + def get(self, request, course_key_string, *args, **kwargs): + """ + Return response to a GET request. + """ + course_id = CourseKey.from_string(course_key_string) + resp = { + 'block_id': None, + 'section_id': None, + 'unit_id': None, + } + + try: + block_key = get_key_to_last_completed_block(request.user, course_id) + path = path_to_location(modulestore(), block_key, request, full_path=True) + resp['section_id'] = str(path[2]) + resp['unit_id'] = str(path[3]) + resp['block_id'] = str(block_key) + + except UnavailableCompletionData: + pass + + return Response(resp)