Merge pull request #6474 from edx/awais786/course_info_api
Awais786/course info api
This commit is contained in:
1
AUTHORS
1
AUTHORS
@@ -185,3 +185,4 @@ Jim Zheng <jimzheng@stanford.edu>
|
||||
Afzal Wali <afzaledx@edx.org>
|
||||
Julien Romagnoli <julien.romagnoli@fbmx.net>
|
||||
Wenjie Wu <wuwenjie718@gmail.com>
|
||||
Aamir <aamir.nu.206@gmail.com>
|
||||
|
||||
@@ -301,3 +301,7 @@ DEPRECATED_ADVANCED_COMPONENT_TYPES = ENV_TOKENS.get(
|
||||
################ VIDEO UPLOAD PIPELINE ###############
|
||||
|
||||
VIDEO_UPLOAD_PIPELINE = ENV_TOKENS.get('VIDEO_UPLOAD_PIPELINE', VIDEO_UPLOAD_PIPELINE)
|
||||
|
||||
#date format the api will be formatting the datetime values
|
||||
API_DATE_FORMAT = '%Y-%m-%d'
|
||||
API_DATE_FORMAT = ENV_TOKENS.get('API_DATE_FORMAT', API_DATE_FORMAT)
|
||||
|
||||
@@ -792,3 +792,5 @@ ADVANCED_PROBLEM_TYPES = [
|
||||
'boilerplate_name': None,
|
||||
}
|
||||
]
|
||||
#date format the api will be formatting the datetime values
|
||||
API_DATE_FORMAT = '%Y-%m-%d'
|
||||
|
||||
6
common/djangoapps/course_about/__init__.py
Normal file
6
common/djangoapps/course_about/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
API for retrieving Course metadata.
|
||||
This API is not intended for exposing course content, but allowing general access to descriptive course
|
||||
details.
|
||||
|
||||
"""
|
||||
65
common/djangoapps/course_about/api.py
Normal file
65
common/djangoapps/course_about/api.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
The Python API layer of the Course About API. Essentially the middle tier of the project, responsible for all
|
||||
business logic that is not directly tied to the data itself.
|
||||
|
||||
Data access is managed through the configured data module, or defaults to the project's data.py module.
|
||||
|
||||
This API is exposed via the RESTful layer (views.py) but may be used directly in-process.
|
||||
|
||||
"""
|
||||
import logging
|
||||
from django.conf import settings
|
||||
from django.utils import importlib
|
||||
from course_about import errors
|
||||
|
||||
DEFAULT_DATA_API = 'course_about.data'
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_course_about_details(course_id):
|
||||
"""Get course about details for the given course ID.
|
||||
|
||||
Given a Course ID, retrieve all the metadata necessary to fully describe the Course.
|
||||
|
||||
Args:
|
||||
course_id (str): The String representation of a Course ID. Used to look up the requested
|
||||
course.
|
||||
|
||||
Returns:
|
||||
A JSON serializable dictionary of metadata describing the course.
|
||||
|
||||
Example:
|
||||
>>> get_course_about_details('edX/Demo/2014T2')
|
||||
{
|
||||
"advertised_start": "FALL",
|
||||
"announcement": "YYYY-MM-DD",
|
||||
"course_id": "edx/DemoCourse",
|
||||
"course_number": "DEMO101",
|
||||
"start": "YYYY-MM-DD",
|
||||
"end": "YYYY-MM-DD",
|
||||
"effort": "HH:MM",
|
||||
"display_name": "Demo Course",
|
||||
"is_new": true,
|
||||
"media": {
|
||||
"course_image": "/some/image/location.png"
|
||||
},
|
||||
}
|
||||
"""
|
||||
return _data_api().get_course_about_details(course_id)
|
||||
|
||||
|
||||
def _data_api():
|
||||
"""Returns a Data API.
|
||||
This relies on Django settings to find the appropriate data API.
|
||||
|
||||
We retrieve the settings in-line here (rather than using the
|
||||
top-level constant), so that @override_settings will work
|
||||
in the test suite.
|
||||
"""
|
||||
api_path = getattr(settings, "COURSE_ABOUT_DATA_API", DEFAULT_DATA_API)
|
||||
try:
|
||||
return importlib.import_module(api_path)
|
||||
except (ImportError, ValueError):
|
||||
log.exception(u"Could not load module at '{path}'".format(path=api_path))
|
||||
raise errors.CourseAboutApiLoadError(api_path)
|
||||
53
common/djangoapps/course_about/data.py
Normal file
53
common/djangoapps/course_about/data.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Data Aggregation Layer for the Course About API.
|
||||
This is responsible for combining data from the following resources:
|
||||
* CourseDescriptor
|
||||
* CourseAboutDescriptor
|
||||
"""
|
||||
import logging
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from course_about.serializers import serialize_content
|
||||
from course_about.errors import CourseNotFoundError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ABOUT_ATTRIBUTES = [
|
||||
'effort'
|
||||
]
|
||||
|
||||
|
||||
def get_course_about_details(course_id): # pylint: disable=unused-argument
|
||||
"""
|
||||
Return course information for a given course id.
|
||||
Args:
|
||||
course_id(str) : The course id to retrieve course information for.
|
||||
Returns:
|
||||
Serializable dictionary of the Course About Information.
|
||||
Raises:
|
||||
CourseNotFoundError
|
||||
"""
|
||||
try:
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course_descriptor = modulestore().get_course(course_key)
|
||||
if course_descriptor is None:
|
||||
raise CourseNotFoundError("course not found")
|
||||
except InvalidKeyError as err:
|
||||
raise CourseNotFoundError(err.message)
|
||||
about_descriptor = {}
|
||||
for attribute in ABOUT_ATTRIBUTES:
|
||||
about_descriptor[attribute] = _fetch_course_detail(course_key, attribute)
|
||||
return serialize_content(course_descriptor=course_descriptor, about_descriptor=about_descriptor)
|
||||
|
||||
|
||||
def _fetch_course_detail(course_key, attribute):
|
||||
"""
|
||||
Fetch the course about attribute for the given course's attribute from persistence and return its value.
|
||||
"""
|
||||
usage_key = course_key.make_usage_key('about', attribute)
|
||||
try:
|
||||
value = modulestore().get_item(usage_key).data
|
||||
except ItemNotFoundError:
|
||||
value = None
|
||||
return value
|
||||
23
common/djangoapps/course_about/errors.py
Normal file
23
common/djangoapps/course_about/errors.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Contains all the errors associated with the Course About API.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class CourseAboutError(Exception):
|
||||
"""Generic Course About Error"""
|
||||
|
||||
def __init__(self, msg, data=None):
|
||||
super(CourseAboutError, self).__init__(msg)
|
||||
# Corresponding information to help resolve the error.
|
||||
self.data = data
|
||||
|
||||
|
||||
class CourseAboutApiLoadError(CourseAboutError):
|
||||
"""The data API could not be loaded. """
|
||||
pass
|
||||
|
||||
|
||||
class CourseNotFoundError(CourseAboutError):
|
||||
"""The Course Not Found. """
|
||||
pass
|
||||
6
common/djangoapps/course_about/models.py
Normal file
6
common/djangoapps/course_about/models.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
A models.py is required to make this an app (until we move to Django 1.7)
|
||||
The Course About API is responsible for aggregating descriptive course information into a single response.
|
||||
This should eventually hold some initial Marketing Meta Data objects that are platform-specific.
|
||||
|
||||
"""
|
||||
59
common/djangoapps/course_about/serializers.py
Normal file
59
common/djangoapps/course_about/serializers.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Serializers for all Course Descriptor and Course About Descriptor related return objects.
|
||||
|
||||
"""
|
||||
from util.parsing_utils import course_image_url
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def serialize_content(course_descriptor, about_descriptor):
|
||||
"""Serialize the course descriptor and about descriptor
|
||||
|
||||
Returns a serialized representation of the course_descriptor and about_descriptor
|
||||
|
||||
Args:
|
||||
course_descriptor(CourseDescriptor) : course descriptor object
|
||||
about_descriptor(dict) : Dictionary of CourseAboutDescriptor objects
|
||||
|
||||
Returns:
|
||||
Serializable dictionary of course information.
|
||||
|
||||
"""
|
||||
date_format = getattr(settings, 'API_DATE_FORMAT', '%Y-%m-%d')
|
||||
data = dict({"media": {}})
|
||||
data['display_name'] = getattr(course_descriptor, 'display_name', None)
|
||||
start = getattr(course_descriptor, 'start', None)
|
||||
end = getattr(course_descriptor, 'end', None)
|
||||
announcement = getattr(course_descriptor, 'announcement', None)
|
||||
data['start'] = start.strftime(date_format) if start else None
|
||||
data['end'] = end.strftime(date_format) if end else None
|
||||
data["announcement"] = announcement.strftime(date_format) if announcement else None
|
||||
data['advertised_start'] = getattr(course_descriptor, 'advertised_start', None)
|
||||
data['is_new'] = getattr(course_descriptor, 'is_new', None)
|
||||
image_url = ''
|
||||
if hasattr(course_descriptor, 'course_image') and course_descriptor.course_image:
|
||||
image_url = course_image_url(course_descriptor)
|
||||
data['course_number'] = course_descriptor.location.course
|
||||
data['course_id'] = unicode(course_descriptor.id)
|
||||
data['media']['course_image'] = image_url
|
||||
# Following code is getting the course about descriptor information
|
||||
course_about_data = _course_about_serialize_content(about_descriptor)
|
||||
data.update(course_about_data)
|
||||
return data
|
||||
|
||||
|
||||
def _course_about_serialize_content(about_descriptor):
|
||||
"""Serialize the course about descriptor
|
||||
|
||||
Returns a serialized representation of the about_descriptor
|
||||
|
||||
Args:
|
||||
course_descriptor(dict) : dictionary of course descriptor object
|
||||
|
||||
Returns:
|
||||
Serialize data for about descriptor.
|
||||
|
||||
"""
|
||||
data = dict()
|
||||
data["effort"] = about_descriptor.get("effort", None)
|
||||
return data
|
||||
4
common/djangoapps/course_about/tests/__init__.py
Normal file
4
common/djangoapps/course_about/tests/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Packages all tests relative to the Course About API.
|
||||
|
||||
"""
|
||||
74
common/djangoapps/course_about/tests/test_data.py
Normal file
74
common/djangoapps/course_about/tests/test_data.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Tests specific to the Data Aggregation Layer of the Course About API.
|
||||
|
||||
"""
|
||||
import unittest
|
||||
from django.test.utils import override_settings
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase, mixed_store_config
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from student.tests.factories import UserFactory
|
||||
from course_about import data
|
||||
from course_about.errors import CourseNotFoundError
|
||||
from nose.tools import raises
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from datetime import datetime
|
||||
|
||||
# Since we don't need any XML course fixtures, use a modulestore configuration
|
||||
# that disables the XML modulestore.
|
||||
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class CourseAboutDataTest(ModuleStoreTestCase):
|
||||
"""
|
||||
Test course enrollment data aggregation.
|
||||
|
||||
"""
|
||||
USERNAME = "Bob"
|
||||
EMAIL = "bob@example.com"
|
||||
PASSWORD = "edx"
|
||||
|
||||
def setUp(self):
|
||||
"""Create a course and user, then log in. """
|
||||
super(CourseAboutDataTest, self).setUp()
|
||||
self.course = CourseFactory.create()
|
||||
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
|
||||
self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
||||
|
||||
def test_get_course_about_details(self):
|
||||
course_info = data.get_course_about_details(unicode(self.course.id))
|
||||
self.assertIsNotNone(course_info)
|
||||
|
||||
def test_get_course_about_valid_date(self):
|
||||
module_store = modulestore()
|
||||
self.course.start = datetime.now()
|
||||
self.course.end = datetime.now()
|
||||
self.course.announcement = datetime.now()
|
||||
module_store.update_item(self.course, self.user.id)
|
||||
course_info = data.get_course_about_details(unicode(self.course.id))
|
||||
self.assertIsNotNone(course_info["start"])
|
||||
self.assertIsNotNone(course_info["end"])
|
||||
self.assertIsNotNone(course_info["announcement"])
|
||||
|
||||
def test_get_course_about_none_date(self):
|
||||
module_store = modulestore()
|
||||
self.course.start = None
|
||||
self.course.end = None
|
||||
self.course.announcement = None
|
||||
module_store.update_item(self.course, self.user.id)
|
||||
course_info = data.get_course_about_details(unicode(self.course.id))
|
||||
self.assertIsNone(course_info["start"])
|
||||
self.assertIsNone(course_info["end"])
|
||||
self.assertIsNone(course_info["announcement"])
|
||||
|
||||
@raises(CourseNotFoundError)
|
||||
def test_non_existent_course(self):
|
||||
data.get_course_about_details("this/is/bananas")
|
||||
|
||||
@raises(CourseNotFoundError)
|
||||
def test_invalid_key(self):
|
||||
data.get_course_about_details("invalid:key:k")
|
||||
164
common/djangoapps/course_about/tests/test_views.py
Normal file
164
common/djangoapps/course_about/tests/test_views.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Tests for user enrollment.
|
||||
"""
|
||||
import ddt
|
||||
import json
|
||||
import unittest
|
||||
|
||||
from django.test.utils import override_settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase, mixed_store_config
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, CourseAboutFactory
|
||||
from student.tests.factories import UserFactory
|
||||
from util.parsing_utils import course_image_url
|
||||
from course_about import api
|
||||
from course_about.errors import CourseNotFoundError, CourseAboutError
|
||||
from mock import patch
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from datetime import datetime
|
||||
|
||||
# Since we don't need any XML course fixtures, use a modulestore configuration
|
||||
# that disables the XML modulestore.
|
||||
|
||||
MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class CourseInfoTest(ModuleStoreTestCase, APITestCase):
|
||||
"""
|
||||
Test course information.
|
||||
"""
|
||||
USERNAME = "Bob"
|
||||
EMAIL = "bob@example.com"
|
||||
PASSWORD = "edx"
|
||||
|
||||
def setUp(self):
|
||||
""" Create a course"""
|
||||
super(CourseInfoTest, self).setUp()
|
||||
|
||||
self.course = CourseFactory.create()
|
||||
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
|
||||
self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
||||
|
||||
def test_user_not_authenticated(self):
|
||||
# Log out, so we're no longer authenticated
|
||||
self.client.logout()
|
||||
|
||||
resp = self.client.get(
|
||||
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
|
||||
)
|
||||
resp_data = json.loads(resp.content)
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
self.assertIsNotNone(resp_data)
|
||||
|
||||
def test_with_valid_course_id(self):
|
||||
resp = self.client.get(
|
||||
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
|
||||
)
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_with_invalid_course_id(self):
|
||||
resp = self.client.get(
|
||||
reverse('courseabout', kwargs={"course_id": 'not/a/validkey'})
|
||||
)
|
||||
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_get_course_details_all_attributes(self):
|
||||
kwargs = dict()
|
||||
kwargs["course_id"] = self.course.id
|
||||
kwargs["course_runtime"] = self.course.runtime
|
||||
CourseAboutFactory.create(**kwargs)
|
||||
resp = self.client.get(
|
||||
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
|
||||
)
|
||||
resp_data = json.loads(resp.content)
|
||||
all_attributes = ['display_name', 'start', 'end', 'announcement', 'advertised_start', 'is_new', 'course_number',
|
||||
'course_id',
|
||||
'effort', 'media', 'course_image']
|
||||
for attr in all_attributes:
|
||||
self.assertIn(attr, str(resp_data))
|
||||
|
||||
def test_get_course_about_valid_date(self):
|
||||
module_store = modulestore()
|
||||
self.course.start = datetime.now()
|
||||
self.course.end = datetime.now()
|
||||
self.course.announcement = datetime.now()
|
||||
module_store.update_item(self.course, self.user.id)
|
||||
resp = self.client.get(
|
||||
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
|
||||
)
|
||||
course_info = json.loads(resp.content)
|
||||
self.assertIsNotNone(course_info["start"])
|
||||
self.assertIsNotNone(course_info["end"])
|
||||
self.assertIsNotNone(course_info["announcement"])
|
||||
|
||||
def test_get_course_about_none_date(self):
|
||||
module_store = modulestore()
|
||||
self.course.start = None
|
||||
self.course.end = None
|
||||
self.course.announcement = None
|
||||
module_store.update_item(self.course, self.user.id)
|
||||
resp = self.client.get(
|
||||
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
|
||||
)
|
||||
course_info = json.loads(resp.content)
|
||||
self.assertIsNone(course_info["start"])
|
||||
self.assertIsNone(course_info["end"])
|
||||
self.assertIsNone(course_info["announcement"])
|
||||
|
||||
def test_get_course_details(self):
|
||||
kwargs = dict()
|
||||
kwargs["course_id"] = self.course.id
|
||||
kwargs["course_runtime"] = self.course.runtime
|
||||
kwargs["user_id"] = self.user.id
|
||||
CourseAboutFactory.create(**kwargs)
|
||||
resp = self.client.get(
|
||||
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
|
||||
)
|
||||
self.assertEqual(resp.status_code, status.HTTP_200_OK)
|
||||
resp_data = json.loads(resp.content)
|
||||
|
||||
self.assertEqual(unicode(self.course.id), resp_data['course_id'])
|
||||
self.assertIn('Run', resp_data['display_name'])
|
||||
|
||||
url = course_image_url(self.course)
|
||||
self.assertEquals(url, resp_data['media']['course_image'])
|
||||
|
||||
@patch.object(api, "get_course_about_details")
|
||||
def test_get_enrollment_course_not_found_error(self, mock_get_course_about_details):
|
||||
mock_get_course_about_details.side_effect = CourseNotFoundError("Something bad happened.")
|
||||
resp = self.client.get(
|
||||
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
|
||||
)
|
||||
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@patch.object(api, "get_course_about_details")
|
||||
def test_get_enrollment_invalid_key_error(self, mock_get_course_about_details):
|
||||
mock_get_course_about_details.side_effect = CourseNotFoundError('a/a/a', "Something bad happened.")
|
||||
resp = self.client.get(
|
||||
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
|
||||
)
|
||||
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@patch.object(api, "get_course_about_details")
|
||||
def test_get_enrollment_internal_error(self, mock_get_course_about_details):
|
||||
mock_get_course_about_details.side_effect = CourseAboutError('error')
|
||||
resp = self.client.get(
|
||||
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
|
||||
)
|
||||
self.assertEqual(resp.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@override_settings(COURSE_ABOUT_DATA_API='foo')
|
||||
def test_data_api_config_error(self):
|
||||
# Enroll in the course and verify the URL we get sent to
|
||||
resp = self.client.get(
|
||||
reverse('courseabout', kwargs={"course_id": unicode(self.course.id)})
|
||||
)
|
||||
self.assertEqual(resp.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
15
common/djangoapps/course_about/urls.py
Normal file
15
common/djangoapps/course_about/urls.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
URLs for exposing the RESTful HTTP endpoints for the Course About API.
|
||||
|
||||
"""
|
||||
from django.conf import settings
|
||||
from django.conf.urls import patterns, url
|
||||
from course_about.views import CourseAboutView
|
||||
|
||||
urlpatterns = patterns(
|
||||
'course_about.views',
|
||||
url(
|
||||
r'^{course_key}'.format(course_key=settings.COURSE_ID_PATTERN),
|
||||
CourseAboutView.as_view(), name="courseabout"
|
||||
),
|
||||
)
|
||||
63
common/djangoapps/course_about/views.py
Normal file
63
common/djangoapps/course_about/views.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Implementation of the RESTful endpoints for the Course About API.
|
||||
|
||||
"""
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
from course_about import api
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from course_about.errors import CourseNotFoundError, CourseAboutError
|
||||
|
||||
|
||||
class CourseAboutThrottle(UserRateThrottle):
|
||||
"""Limit the number of requests users can make to the Course About API."""
|
||||
# TODO Limit based on expected throughput # pylint: disable=fixme
|
||||
rate = '50/second'
|
||||
|
||||
|
||||
class CourseAboutView(APIView):
|
||||
""" RESTful Course About API view.
|
||||
|
||||
Used to retrieve JSON serialized Course About information.
|
||||
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
throttle_classes = CourseAboutThrottle,
|
||||
|
||||
def get(self, request, course_id=None): # pylint: disable=unused-argument
|
||||
"""Read course information.
|
||||
|
||||
HTTP Endpoint for course info api.
|
||||
|
||||
Args:
|
||||
Course Id = URI element specifying the course location. Course information will be
|
||||
returned for this particular course.
|
||||
|
||||
Return:
|
||||
A JSON serialized representation of the course information
|
||||
|
||||
"""
|
||||
try:
|
||||
return Response(api.get_course_about_details(course_id))
|
||||
except CourseNotFoundError:
|
||||
return Response(
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
data={
|
||||
"message": (
|
||||
u"An error occurred while retrieving course information"
|
||||
u" for course '{course_id}' no course found"
|
||||
).format(course_id=course_id)
|
||||
}
|
||||
)
|
||||
except CourseAboutError:
|
||||
return Response(
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
data={
|
||||
"message": (
|
||||
u"An error occurred while retrieving course information"
|
||||
u" for course '{course_id}'"
|
||||
).format(course_id=course_id)
|
||||
}
|
||||
)
|
||||
17
common/djangoapps/util/parsing_utils.py
Normal file
17
common/djangoapps/util/parsing_utils.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Utility function for some parsing stuff
|
||||
"""
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
|
||||
|
||||
def course_image_url(course):
|
||||
"""
|
||||
Return url of course image.
|
||||
Args:
|
||||
course(CourseDescriptor) : The course id to retrieve course image url.
|
||||
Returns:
|
||||
Absolute url of course image.
|
||||
"""
|
||||
loc = StaticContent.compute_location(course.id, course.course_image)
|
||||
url = StaticContent.serialize_asset_key_with_slash(loc)
|
||||
return url
|
||||
@@ -15,6 +15,7 @@ from mock import Mock, patch
|
||||
from nose.tools import assert_less_equal, assert_greater_equal
|
||||
import factory
|
||||
import threading
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
class Dummy(object):
|
||||
@@ -34,6 +35,7 @@ class XModuleFactory(Factory):
|
||||
@lazy_attribute
|
||||
def modulestore(self):
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
return modulestore()
|
||||
|
||||
|
||||
@@ -332,19 +334,55 @@ def check_mongo_calls(num_finds=0, num_sends=None):
|
||||
the given int value.
|
||||
"""
|
||||
with check_sum_of_calls(
|
||||
pymongo.message,
|
||||
['query', 'get_more'],
|
||||
num_finds,
|
||||
num_finds
|
||||
pymongo.message,
|
||||
['query', 'get_more'],
|
||||
num_finds,
|
||||
num_finds
|
||||
):
|
||||
if num_sends is not None:
|
||||
with check_sum_of_calls(
|
||||
pymongo.message,
|
||||
# mongo < 2.6 uses insert, update, delete and _do_batched_insert. >= 2.6 _do_batched_write
|
||||
['insert', 'update', 'delete', '_do_batched_write_command', '_do_batched_insert', ],
|
||||
num_sends,
|
||||
num_sends
|
||||
pymongo.message,
|
||||
# mongo < 2.6 uses insert, update, delete and _do_batched_insert. >= 2.6 _do_batched_write
|
||||
['insert', 'update', 'delete', '_do_batched_write_command', '_do_batched_insert', ],
|
||||
num_sends,
|
||||
num_sends
|
||||
):
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
# This dict represents the attribute keys for a course's 'about' info.
|
||||
# Note: The 'video' attribute is intentionally excluded as it must be
|
||||
# handled separately; its value maps to an alternate key name.
|
||||
# Reference : cms/djangoapps/models/settings/course_details.py
|
||||
|
||||
ABOUT_ATTRIBUTES = {
|
||||
'effort': "Testing effort",
|
||||
}
|
||||
|
||||
|
||||
class CourseAboutFactory(XModuleFactory):
|
||||
"""
|
||||
Factory for XModule course about.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _create(cls, target_class, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Uses **kwargs:
|
||||
|
||||
effort: effor information
|
||||
|
||||
video : video link
|
||||
"""
|
||||
user_id = kwargs.pop('user_id', None)
|
||||
course_id, course_runtime = kwargs.pop("course_id"), kwargs.pop("course_runtime")
|
||||
store = modulestore()
|
||||
for about_key in ABOUT_ATTRIBUTES:
|
||||
about_item = store.create_xblock(course_runtime, course_id, 'about', about_key)
|
||||
about_item.data = ABOUT_ATTRIBUTES[about_key]
|
||||
store.update_item(about_item, user_id, allow_not_found=True)
|
||||
about_item = store.create_xblock(course_runtime, course_id, 'about', 'video')
|
||||
about_item.data = "www.youtube.com/embed/testing-video-link"
|
||||
store.update_item(about_item, user_id, allow_not_found=True)
|
||||
|
||||
@@ -471,3 +471,7 @@ REGISTRATION_CODE_LENGTH = ENV_TOKENS.get('REGISTRATION_CODE_LENGTH', 8)
|
||||
# REGISTRATION CODES DISPLAY INFORMATION
|
||||
INVOICE_CORP_ADDRESS = ENV_TOKENS.get('INVOICE_CORP_ADDRESS', INVOICE_CORP_ADDRESS)
|
||||
INVOICE_PAYMENT_INSTRUCTIONS = ENV_TOKENS.get('INVOICE_PAYMENT_INSTRUCTIONS', INVOICE_PAYMENT_INSTRUCTIONS)
|
||||
|
||||
#date format the api will be formatting the datetime values
|
||||
API_DATE_FORMAT = '%Y-%m-%d'
|
||||
API_DATE_FORMAT = ENV_TOKENS.get('API_DATE_FORMAT', API_DATE_FORMAT)
|
||||
|
||||
@@ -1943,3 +1943,6 @@ COURSE_CATALOG_VISIBILITY_PERMISSION = 'see_exists'
|
||||
# which access.py permission name to check in order to determine if a course about page is
|
||||
# visible. We default this to the legacy permission 'see_exists'.
|
||||
COURSE_ABOUT_VISIBILITY_PERMISSION = 'see_exists'
|
||||
|
||||
#date format the api will be formatting the datetime values
|
||||
API_DATE_FORMAT = '%Y-%m-%d'
|
||||
|
||||
@@ -76,6 +76,9 @@ urlpatterns = ('', # nopep8
|
||||
# Enrollment API RESTful endpoints
|
||||
url(r'^api/enrollment/v1/', include('enrollment.urls')),
|
||||
|
||||
# CourseInfo API RESTful endpoints
|
||||
url(r'^api/course/details/v0/', include('course_about.urls')),
|
||||
|
||||
)
|
||||
|
||||
if settings.FEATURES["ENABLE_MOBILE_REST_API"]:
|
||||
|
||||
Reference in New Issue
Block a user