Skip to content

Commit

Permalink
feat: [AXIM-26] Extended BlocksInCourseView API
Browse files Browse the repository at this point in the history
  • Loading branch information
KyryloKireiev authored and oksana-slu committed Sep 22, 2023
1 parent f9148f0 commit 0807140
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 2 deletions.
47 changes: 47 additions & 0 deletions lms/djangoapps/mobile_api/course_info/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
3 changes: 2 additions & 1 deletion lms/djangoapps/mobile_api/course_info/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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"),
]
120 changes: 120 additions & 0 deletions lms/djangoapps/mobile_api/course_info/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -166,3 +170,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=<course_id>
GET /api/mobile/{api_version}/course_info/blocks/?course_id=<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
1 change: 1 addition & 0 deletions lms/djangoapps/mobile_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
API_V05 = 'v0.5'
API_V1 = 'v1'
API_V2 = 'v2'
API_V3 = 'v3'


def parsed_version(version):
Expand Down
2 changes: 1 addition & 1 deletion lms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@

if settings.FEATURES.get('ENABLE_MOBILE_REST_API'):
urlpatterns += [
re_path(r'^api/mobile/(?P<api_version>v(2|1|0.5))/', include('lms.djangoapps.mobile_api.urls')),
re_path(r'^api/mobile/(?P<api_version>v(3|2|1|0.5))/', include('lms.djangoapps.mobile_api.urls')),
]

if settings.FEATURES.get('ENABLE_OPENBADGES'):
Expand Down

0 comments on commit 0807140

Please sign in to comment.