Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 122 additions & 2 deletions course_discovery/apps/core/api_client/lms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
API Client for LMS.
"""
import logging
from urllib.parse import urljoin
from typing import Optional, Union
from urllib.parse import urlencode, urljoin

from django.conf import settings
from django.core.cache import cache
from edx_django_utils.cache import get_cache_key
from requests.exceptions import RequestException
Expand Down Expand Up @@ -51,7 +53,7 @@ def get_api_access_request(self, user):
"contacted": True
}
"""
resource = 'api-admin/api/v1/api_access_request/'
resource = settings.LMS_API_URLS['api_access_request']
query_parameters = {
'user__username': user.username
}
Expand Down Expand Up @@ -89,3 +91,121 @@ def get_api_access_request(self, user):
exception.__class__.__name__, user.username)

return api_access_request

def _get_blocks_data(
self,
item_id: str,
cache_key: str,
query_parameters: Union[str, dict],
resource: str,
response_root_key: Optional[str] = None,
):
"""
Helper function to fetch blocks based on given resourse and item_id.

Args:
item_id (str): course_id or block_id
cache_key (str): cache key
query_parameters (Union[str, dict]): query parameters for the request
resource (str): resource url

Returns:
(dict): dict with xblock data
"""
cached_blocks = cache.get(cache_key)

if cached_blocks is SENTINEL_NO_RESULT:
return None

if cached_blocks:
return cached_blocks

blocks = None
try:
resource_url = urljoin(self.lms_url, resource)
response = self.client.get(resource_url, params=query_parameters)
response.raise_for_status()
blocks = response.json()
if response_root_key:
blocks = blocks[response_root_key]
Comment thread
navinkarkera marked this conversation as resolved.
if blocks:
cache.set(cache_key, blocks, ONE_HOUR)
else:
cache.set(cache_key, SENTINEL_NO_RESULT, ONE_HOUR)
logger.info('No blocks found for [%s].', item_id)

except (RequestException, KeyError) as exception:
Comment thread
navinkarkera marked this conversation as resolved.
cache.set(cache_key, SENTINEL_NO_RESULT, ONE_MINUTE)
logger.exception('%s: Failed to fetch blocks from LMS for [%s].',
exception.__class__.__name__, item_id)

return blocks

def get_course_blocks_data(self, course_id: str, **kwargs):
"""
Get all xblocks under a given course.

Args:
course_id (str): course key
**kwargs: Can be used to pass additional query params to api

Returns:
(dict): dict with xblock data
"""
resource = settings.LMS_API_URLS['blocks']
query_parameters = {
'course_id': course_id,
'all_blocks': True,
'depth': 'all',
'requested_fields': 'children',
**kwargs,
}
encoded_query_parameters = urlencode(query_parameters, safe=':')
cache_key = get_cache_key(course_id=course_id, resource=resource)
return self._get_blocks_data(
course_id,
cache_key,
encoded_query_parameters,
resource,
response_root_key='blocks',
)

def get_blocks_data(self, block_id: str, **kwargs):
"""
Get xblock data for given block_id or all blocks for given course_id.

Args:
block_id (str): usage key
**kwargs: Can be used to pass additional query params to api

Returns:
(dict): dict with xblock data
"""
resource = settings.LMS_API_URLS['blocks'] + block_id
query_parameters = {
'all_blocks': True,
'depth': 'all',
'requested_fields': 'children',
**kwargs,
}
cache_key = get_cache_key(block_id=block_id, resource=resource)
return self._get_blocks_data(block_id, cache_key, query_parameters, resource, response_root_key='blocks')

def get_blocks_metadata(self, block_id: str, **kwargs):
"""
Get xblock metadata for given block_id.

Args:
block_id (str): usage key
**kwargs: Can be used to pass additional query params to api

Returns:
(dict): dict with xblock data
"""
resource = settings.LMS_API_URLS['block_metadata'] + block_id
query_parameters = {
'include': 'index_dictionary',
**kwargs,
}
cache_key = get_cache_key(block_id=block_id, resource=resource)
return self._get_blocks_data(block_id, cache_key, query_parameters, resource)
88 changes: 88 additions & 0 deletions course_discovery/apps/core/tests/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,91 @@ def mock_api_access_request_with_invalid_data(self, lms_url, user, status=200, r
content_type='application/json',
status=status,
)

def mock_blocks_data_request(self, lms_url, override_blocks=None, status=200):
"""
Mock the blocks data requests endpoint response of the LMS.
"""
data = {
'root': 'block-v1:edX+DemoX+Demo_Course+type@course+block@course',
'blocks': {
'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4': {
'id': 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',
'block_id': '030e35c4756a4ddc8d40b95fbbfff4d4',
'type': 'html',
'display_name': 'Blank HTML Page',
},
'block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd': {
'id': 'block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd',
'block_id': '0b9e39477cf34507a7a48f74be381fdd',
'type': 'video',
'display_name': 'Welcome!',
},
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc': {
'id': 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc',
'block_id': 'vertical_0270f6de40fc',
'type': 'vertical',
'display_name': 'Introduction: Video and Sequences',
'children': [
'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',
'block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd'
]
},
}
}
if override_blocks is not None:
data['blocks'] = override_blocks

responses.add(
responses.GET,
lms_url,
body=json.dumps(data),
content_type='application/json',
status=status,
)
return data

def mock_block_metadata_request(self, base_url, status=200):
data = {
'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4': {
'id': 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',
'type': 'html',
'index_dictionary': {
'content': {
'display_name': 'Blank HTML Page',
'html_content': 'Welcome to the Open edX Demo Course Introduction.'
},
'content_type': 'Text'
}
},
'block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd': {
'id': 'block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd',
'type': 'video',
'index_dictionary': {
'content': {
'display_name': 'Welcome!',
'transcript_en': ' ERIC: Hi, and welcome to the edX demonstration course.'
},
'content_type': 'Video'
}
},
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc': {
'id': 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc',
'type': 'vertical',
'index_dictionary': {
'content': {
'display_name': 'Introduction: Video and Sequences'
},
'content_type': 'Sequence'
},
}
}
for block_id, block_body in data.items():
responses.add(
responses.GET,
base_url + block_id,
body=json.dumps(block_body),
content_type='application/json',
status=status,
)
return data
52 changes: 52 additions & 0 deletions course_discovery/apps/core/tests/test_api_clients.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
from urllib.parse import urljoin

import responses
from django.conf import settings
from django.test import TestCase

from course_discovery.apps.core.api_client import lms
Expand Down Expand Up @@ -41,6 +43,11 @@ def setUp(self):
'site': 1,
'contacted': True
}
self.block_id = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc'
resource = settings.LMS_API_URLS['blocks']
self.block_resource_url = urljoin(self.partner.lms_url, resource + self.block_id)
self.course_resource_url = urljoin(self.partner.lms_url, resource)
self.block_metadata_base_url = urljoin(self.partner.lms_url, settings.LMS_API_URLS['block_metadata'])

@responses.activate
def test_get_api_access_request(self):
Expand Down Expand Up @@ -185,3 +192,48 @@ def test_get_api_access_request_with_multiple_records(self):
assert self.lms.get_api_access_request(self.user)['company_name'] == 'Test Company'
assert 'Multiple ApiAccessRequest models returned from LMS API for user [%s].' % self.user.username in \
self.log_messages['warning']

@responses.activate
def test_get_course_blocks_data(self):
"""
Verify that `get_course_blocks_data` returns correct value.
"""
data = self.mock_blocks_data_request(self.course_resource_url)
assert self.lms.get_course_blocks_data('dummy-course-id') == data['blocks']

@responses.activate
def test_get_blocks_data(self):
"""
Verify that `get_blocks_data` returns correct value.
"""
data = self.mock_blocks_data_request(self.block_resource_url)
assert self.lms.get_blocks_data(self.block_id) == data['blocks']

@responses.activate
def test_get_block_metadata(self):
"""
Verify that `get_blocks_metadata` returns correct value.
"""
data = self.mock_block_metadata_request(self.block_metadata_base_url)
assert self.lms.get_blocks_metadata(self.block_id) == data[self.block_id]

@responses.activate
def test_get_blocks_data_with_no_results(self):
"""
Verify that `get_blocks_data` returns None when
API returns no results.
"""
self.mock_blocks_data_request(self.block_resource_url, override_blocks={})
assert not self.lms.get_blocks_data(self.block_id)
assert 'No blocks found for [%s].' % self.block_id in self.log_messages['info']

@responses.activate
def test_get_blocks_data_cache_hit(self):
"""
Verify that `get_blocks_data` returns the correct value and then
returns the cached results on another call with the same block_id.
"""
data = self.mock_blocks_data_request(self.block_resource_url)
assert self.lms.get_blocks_data(self.block_id) == data['blocks']
assert self.lms.get_blocks_data(self.block_id) == data['blocks']
assert len(responses.calls) == 1
Loading