From 1e7d567b58f800a632734fbc9d759396c21dc037 Mon Sep 17 00:00:00 2001 From: stephensanchez Date: Thu, 18 Dec 2014 20:01:56 +0000 Subject: [PATCH] Initial framework for the Course About API. ECOM-248 Course Info API. Basic functionality implemented. ECOM-248 adding factory for about descriptor and test cases for course info api ECOM-248 adding test cases for couse info api. ECOM-248 re-factoring code. updating test cases. Tests for course_about data module ECOM-248 Adding test cases for the exceptions. ECOM-248 re-factoring code. fixing quality issues. ECOM-248 fixing test cases and moved parse video method into utils. added github username in authors ECOM-248 removed merging issue of test_data ECOM-248 removed unused files --- AUTHORS | 1 + cms/envs/aws.py | 4 + cms/envs/common.py | 2 + common/djangoapps/course_about/__init__.py | 6 + common/djangoapps/course_about/api.py | 65 +++++++ common/djangoapps/course_about/data.py | 53 ++++++ common/djangoapps/course_about/errors.py | 23 +++ common/djangoapps/course_about/models.py | 6 + common/djangoapps/course_about/serializers.py | 59 +++++++ .../djangoapps/course_about/tests/__init__.py | 4 + .../course_about/tests/test_data.py | 74 ++++++++ .../course_about/tests/test_views.py | 164 ++++++++++++++++++ common/djangoapps/course_about/urls.py | 15 ++ common/djangoapps/course_about/views.py | 63 +++++++ common/djangoapps/util/parsing_utils.py | 17 ++ .../xmodule/modulestore/tests/factories.py | 56 +++++- lms/envs/aws.py | 4 + lms/envs/common.py | 3 + lms/urls.py | 3 + 19 files changed, 613 insertions(+), 9 deletions(-) create mode 100644 common/djangoapps/course_about/__init__.py create mode 100644 common/djangoapps/course_about/api.py create mode 100644 common/djangoapps/course_about/data.py create mode 100644 common/djangoapps/course_about/errors.py create mode 100644 common/djangoapps/course_about/models.py create mode 100644 common/djangoapps/course_about/serializers.py create mode 100644 common/djangoapps/course_about/tests/__init__.py create mode 100644 common/djangoapps/course_about/tests/test_data.py create mode 100644 common/djangoapps/course_about/tests/test_views.py create mode 100644 common/djangoapps/course_about/urls.py create mode 100644 common/djangoapps/course_about/views.py create mode 100644 common/djangoapps/util/parsing_utils.py diff --git a/AUTHORS b/AUTHORS index f4ab0cf717..0b3ddae487 100644 --- a/AUTHORS +++ b/AUTHORS @@ -185,3 +185,4 @@ Jim Zheng Afzal Wali Julien Romagnoli Wenjie Wu +Aamir diff --git a/cms/envs/aws.py b/cms/envs/aws.py index d7edc052c2..a4c5947b7c 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -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) diff --git a/cms/envs/common.py b/cms/envs/common.py index 2fa3305b2b..d59544ee55 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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' diff --git a/common/djangoapps/course_about/__init__.py b/common/djangoapps/course_about/__init__.py new file mode 100644 index 0000000000..1d03d49c45 --- /dev/null +++ b/common/djangoapps/course_about/__init__.py @@ -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. + +""" diff --git a/common/djangoapps/course_about/api.py b/common/djangoapps/course_about/api.py new file mode 100644 index 0000000000..8c3187cfdb --- /dev/null +++ b/common/djangoapps/course_about/api.py @@ -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) diff --git a/common/djangoapps/course_about/data.py b/common/djangoapps/course_about/data.py new file mode 100644 index 0000000000..fe2213e929 --- /dev/null +++ b/common/djangoapps/course_about/data.py @@ -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 diff --git a/common/djangoapps/course_about/errors.py b/common/djangoapps/course_about/errors.py new file mode 100644 index 0000000000..8d005e6efe --- /dev/null +++ b/common/djangoapps/course_about/errors.py @@ -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 diff --git a/common/djangoapps/course_about/models.py b/common/djangoapps/course_about/models.py new file mode 100644 index 0000000000..b45e419513 --- /dev/null +++ b/common/djangoapps/course_about/models.py @@ -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. + +""" diff --git a/common/djangoapps/course_about/serializers.py b/common/djangoapps/course_about/serializers.py new file mode 100644 index 0000000000..73d35d5471 --- /dev/null +++ b/common/djangoapps/course_about/serializers.py @@ -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 diff --git a/common/djangoapps/course_about/tests/__init__.py b/common/djangoapps/course_about/tests/__init__.py new file mode 100644 index 0000000000..fa9e685e52 --- /dev/null +++ b/common/djangoapps/course_about/tests/__init__.py @@ -0,0 +1,4 @@ +""" +Packages all tests relative to the Course About API. + +""" diff --git a/common/djangoapps/course_about/tests/test_data.py b/common/djangoapps/course_about/tests/test_data.py new file mode 100644 index 0000000000..62810e5516 --- /dev/null +++ b/common/djangoapps/course_about/tests/test_data.py @@ -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") diff --git a/common/djangoapps/course_about/tests/test_views.py b/common/djangoapps/course_about/tests/test_views.py new file mode 100644 index 0000000000..d8a93fb1db --- /dev/null +++ b/common/djangoapps/course_about/tests/test_views.py @@ -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) diff --git a/common/djangoapps/course_about/urls.py b/common/djangoapps/course_about/urls.py new file mode 100644 index 0000000000..63f9561240 --- /dev/null +++ b/common/djangoapps/course_about/urls.py @@ -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" + ), +) diff --git a/common/djangoapps/course_about/views.py b/common/djangoapps/course_about/views.py new file mode 100644 index 0000000000..abae554891 --- /dev/null +++ b/common/djangoapps/course_about/views.py @@ -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) + } + ) diff --git a/common/djangoapps/util/parsing_utils.py b/common/djangoapps/util/parsing_utils.py new file mode 100644 index 0000000000..41cdbe682f --- /dev/null +++ b/common/djangoapps/util/parsing_utils.py @@ -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 diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py index f49f2d2b11..a06bb72799 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/factories.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py @@ -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) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index db98070efc..85a7cd1fd3 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -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) diff --git a/lms/envs/common.py b/lms/envs/common.py index b0c3ae5726..9209eb1514 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -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' diff --git a/lms/urls.py b/lms/urls.py index cff6855eac..72c5102059 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -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"]: