diff --git a/lms/djangoapps/mobile_api/course_info/tests.py b/lms/djangoapps/mobile_api/course_info/tests.py index 086359cafb8f..823588624289 100644 --- a/lms/djangoapps/mobile_api/course_info/tests.py +++ b/lms/djangoapps/mobile_api/course_info/tests.py @@ -15,6 +15,7 @@ from common.djangoapps.student.tests.factories import UserFactory # pylint: disable=unused-import from lms.djangoapps.mobile_api.testutils import MobileAPITestCase, MobileAuthTestMixin, MobileCourseAccessTestMixin from lms.djangoapps.mobile_api.utils import API_V1, API_V05 +from lms.djangoapps.course_api.blocks.tests.test_views import TestBlocksInCourseView from openedx.features.course_experience import ENABLE_COURSE_GOALS from xmodule.html_block import CourseInfoBlock # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order @@ -255,3 +256,49 @@ def test_flag_disabled(self, mock_logger): 'For this mobile request, user activity is not enabled for this user {} and course {}'.format( str(self.user.id), str(self.course.id)) ) + + +@ddt.ddt +class TestBlocksInfoInCourseView(TestBlocksInCourseView): # lint-amnesty, pylint: disable=test-inherits-tests + """ + Test class for BlocksInfoInCourseView + """ + + def setUp(self): + super().setUp() + self.url = reverse('blocks_info_in_course', kwargs={ + 'api_version': 'v3', + }) + + @patch('lms.djangoapps.mobile_api.course_info.views.certificate_downloadable_status') + def test_additional_info_response(self, mock_certificate_downloadable_status): + certificate_url = 'https://test_certificate_url' + mock_certificate_downloadable_status.return_value = { + 'is_downloadable': True, + 'download_url': certificate_url, + } + + expected_image_urls = { + 'image': + { + 'large': '/asset-v1:edX+toy+2012_Fall+type@asset+block@just_a_test.jpg', + 'raw': '/asset-v1:edX+toy+2012_Fall+type@asset+block@just_a_test.jpg', + 'small': '/asset-v1:edX+toy+2012_Fall+type@asset+block@just_a_test.jpg' + } + } + + response = self.verify_response(url=self.url) + + assert response.status_code == 200 + assert response.data['id'] == str(self.course.id) + assert response.data['name'] == self.course.display_name + assert response.data['number'] == self.course.display_number_with_default + assert response.data['org'] == self.course.display_org_with_default + assert response.data['start'] == self.course.start + assert response.data['start_display'] == 'July 17, 2015' + assert response.data['start_type'] == 'timestamp' + assert response.data['end'] == self.course.end + assert response.data['media'] == expected_image_urls + assert response.data['certificate'] == {'url': certificate_url} + assert response.data['is_self_paced'] is False + mock_certificate_downloadable_status.assert_called_once() diff --git a/lms/djangoapps/mobile_api/course_info/urls.py b/lms/djangoapps/mobile_api/course_info/urls.py index 5314b43bc5be..e369930e2550 100644 --- a/lms/djangoapps/mobile_api/course_info/urls.py +++ b/lms/djangoapps/mobile_api/course_info/urls.py @@ -6,7 +6,7 @@ from django.conf import settings from django.urls import path, re_path -from .views import CourseHandoutsList, CourseUpdatesList, CourseGoalsRecordUserActivity +from .views import CourseHandoutsList, CourseUpdatesList, CourseGoalsRecordUserActivity, BlocksInfoInCourseView urlpatterns = [ re_path( @@ -20,4 +20,5 @@ name='course-updates-list' ), path('record_user_activity', CourseGoalsRecordUserActivity.as_view(), name='record_user_activity'), + path('blocks/', BlocksInfoInCourseView.as_view(), name="blocks_info_in_course"), ] diff --git a/lms/djangoapps/mobile_api/course_info/views.py b/lms/djangoapps/mobile_api/course_info/views.py index cb474a5fd754..65a703f98130 100644 --- a/lms/djangoapps/mobile_api/course_info/views.py +++ b/lms/djangoapps/mobile_api/course_info/views.py @@ -12,8 +12,12 @@ from rest_framework.views import APIView from common.djangoapps.static_replace import make_static_urls_absolute +from lms.djangoapps.certificates.api import certificate_downloadable_status from lms.djangoapps.courseware.courses import get_course_info_section_block from lms.djangoapps.course_goals.models import UserActivity +from lms.djangoapps.course_api.blocks.views import BlocksInCourseView +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from openedx.core.lib.api.view_utils import view_auth_classes from openedx.core.lib.xblock_utils import get_course_update_items from openedx.features.course_experience import ENABLE_COURSE_GOALS from ..decorators import mobile_course_access, mobile_view @@ -163,3 +167,119 @@ def post(self, request, *args, **kwargs): # Populate user activity for tracking progress towards a user's course goals UserActivity.record_user_activity(user, course_key) return Response(status=(200)) + + +@view_auth_classes(is_authenticated=False) +class BlocksInfoInCourseView(BlocksInCourseView): + """ + **Use Case** + + Returns the blocks in the course according to the requesting user's access level. + Add to response info fields with information about course + + **Example requests**: + + This api works with all versions {api_version}, you can use: v0.5, v1, v2 or v3 + + GET /api/mobile/{api_version}/course_info/blocks/?course_id= + GET /api/mobile/{api_version}/course_info/blocks/?course_id= + &username=anjali + &depth=all + &requested_fields=graded,format,student_view_multi_device,lti_url + &block_counts=video + &student_view_data=video + &block_types_filter=problem,html + + **Response example** + + Body consists of the following fields: + + root: (str) The ID of the root node of the requested course block structure.\ + blocks: (dict) A dictionary or list, based on the value of the + "return_type" parameter. Maps block usage IDs to a collection of + information about each block. Each block contains the following + fields. + + id: (str) The Course's id (Course Run key) + name: (str) The course's name + number: (str) The course's number + org: (str) The course's organisation + start: (str) Date the course begins, in ISO 8601 notation + start_display: (str) Readably formatted start of the course + start_type: (str) Hint describing how `start_display` is set. One of: + * `"string"`: manually set by the course author + * `"timestamp"`: generated from the `start` timestamp + * `"empty"`: no start date is specified + end: (str) Date the course ends, in ISO 8601 notation + media: (dict) An object that contains named media items. Included here: + * course_image: An image to show for the course. Represented + as an object with the following fields: + * uri: The location of the image + certificate: (dict) Information about the user's earned certificate in the course. + Included here: + * uri: The location of the user's certificate + is_self_paced: (bool) Indicates if the course is self paced + + **Returns** + + * 200 on success with above fields. + * 400 if an invalid parameter was sent or the username was not provided + * 401 unauthorized, the provided access token has expired and is no longer valid + for an authenticated request. + * 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. + """ + + def get_certificate(self, request, course_id): + """Returns the information about the user's certificate in the course.""" + if request.user.is_authenticated: + certificate_info = certificate_downloadable_status(request.user, course_id) + if certificate_info['is_downloadable']: + return { + 'url': request.build_absolute_uri( + certificate_info['download_url'] + ), + } + return {} + + # pylint: disable=arguments-differ + def list(self, request, **kwargs): + """ + REST API endpoint for listing all the blocks information in the course and + information about the course while regarding user access and roles. + + Arguments: + request - Django request object + """ + + response = super().list(request, kwargs) + + if request.GET.get('return_type', 'dict') == 'dict': + course_id = request.query_params.get('course_id', None) + course_key = CourseKey.from_string(course_id) + course_overview = CourseOverview.get_from_id(course_key) + + course_data = { + # identifiers + 'id': course_id, + 'name': course_overview.display_name, + 'number': course_overview.display_number_with_default, + 'org': course_overview.display_org_with_default, + + # dates + 'start': course_overview.start, + 'start_display': course_overview.start_display, + 'start_type': course_overview.start_type, + 'end': course_overview.end, + + # various URLs + 'media': { + 'image': course_overview.image_urls, + }, + 'certificate': self.get_certificate(request, course_key), + 'is_self_paced': course_overview.self_paced + } + + response.data.update(course_data) + return response