diff --git a/cms/envs/aws.py b/cms/envs/aws.py index bae10699c1..b14c726e40 100644 --- a/cms/envs/aws.py +++ b/cms/envs/aws.py @@ -332,10 +332,6 @@ VIDEO_UPLOAD_PIPELINE = ENV_TOKENS.get('VIDEO_UPLOAD_PIPELINE', VIDEO_UPLOAD_PIP PARSE_KEYS = AUTH_TOKENS.get("PARSE_KEYS", {}) -#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) - # Video Caching. Pairing country codes with CDN URLs. # Example: {'CN': 'http://api.xuetangx.com/edx/video?s3_url='} VIDEO_CDN_URL = ENV_TOKENS.get('VIDEO_CDN_URL', {}) diff --git a/cms/envs/common.py b/cms/envs/common.py index ec75f73dcb..83020f746e 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -951,8 +951,6 @@ ADVANCED_PROBLEM_TYPES = [ } ] -#date format the api will be formatting the datetime values -API_DATE_FORMAT = '%Y-%m-%d' # Files and Uploads type filter values diff --git a/common/djangoapps/course_about/__init__.py b/common/djangoapps/course_about/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/djangoapps/course_about/api.py b/common/djangoapps/course_about/api.py deleted file mode 100644 index d5ddd3bf82..0000000000 --- a/common/djangoapps/course_about/api.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -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 django.core.cache import cache -from course_about import errors - -DEFAULT_DATA_API = 'course_about.data' - -COURSE_ABOUT_API_CACHE_PREFIX = 'course_about_api_' - -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. - First its checks the default cache for given course id if its exists then returns - the course otherwise it get the course from module store and set the cache. - By default cache expiry set to 5 minutes. - - 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" - }, - } - """ - cache_key = "{}_{}".format(course_id, COURSE_ABOUT_API_CACHE_PREFIX) - cache_course_info = cache.get(cache_key) - - if cache_course_info: - return cache_course_info - - course_info = _data_api().get_course_about_details(course_id) - time_out = getattr(settings, 'COURSE_INFO_API_CACHE_TIME_OUT', 300) - cache.set(cache_key, course_info, time_out) - - return course_info - - -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 deleted file mode 100644 index 8a5941deca..0000000000 --- a/common/djangoapps/course_about/data.py +++ /dev/null @@ -1,76 +0,0 @@ -"""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', - 'overview', - 'title', - 'university', - 'number', - 'short_description', - 'description', - 'key_dates', - 'video', - 'course_staff_short', - 'course_staff_extended', - 'requirements', - 'syllabus', - 'textbook', - 'faq', - 'more_info', - 'ocw_links', -] - - -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 = { - attribute: _fetch_course_detail(course_key, attribute) - for attribute in ABOUT_ATTRIBUTES - } - - course_info = serialize_content(course_descriptor=course_descriptor, about_descriptor=about_descriptor) - return course_info - - -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 deleted file mode 100644 index 8d005e6efe..0000000000 --- a/common/djangoapps/course_about/errors.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -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 deleted file mode 100644 index b45e419513..0000000000 --- a/common/djangoapps/course_about/models.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -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 deleted file mode 100644 index 9127da580a..0000000000 --- a/common/djangoapps/course_about/serializers.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Serializers for all Course Descriptor and Course About Descriptor related return objects. - -""" -from xmodule.contentstore.content import StaticContent -from django.conf import settings - -DATE_FORMAT = getattr(settings, 'API_DATE_FORMAT', '%Y-%m-%d') - - -def serialize_content(course_descriptor, 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 - return: - serialize data for course information. - """ - data = { - 'media': {}, - 'display_name': getattr(course_descriptor, 'display_name', None), - 'course_number': course_descriptor.location.course, - 'course_id': None, - 'advertised_start': getattr(course_descriptor, 'advertised_start', None), - 'is_new': getattr(course_descriptor, 'is_new', None), - 'start': _formatted_datetime(course_descriptor, 'start'), - 'end': _formatted_datetime(course_descriptor, 'end'), - 'announcement': None, - } - data.update(about_descriptor) - - content_id = unicode(course_descriptor.id) - data["course_id"] = unicode(content_id) - if getattr(course_descriptor, 'course_image', False): - data['media']['course_image'] = course_image_url(course_descriptor) - - announcement = getattr(course_descriptor, 'announcement', None) - data["announcement"] = announcement.strftime(DATE_FORMAT) if announcement else None - - return data - - -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 - - -def _formatted_datetime(course_descriptor, date_type): - """ - Return formatted date. - Args: - course_descriptor(CourseDescriptor) : The CourseDescriptor Object. - date_type (str) : Either start or end. - Returns: - formatted date or None . - """ - course_date_ = getattr(course_descriptor, date_type, None) - return course_date_.strftime(DATE_FORMAT) if course_date_ else None diff --git a/common/djangoapps/course_about/tests/__init__.py b/common/djangoapps/course_about/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/djangoapps/course_about/tests/test_api.py b/common/djangoapps/course_about/tests/test_api.py deleted file mode 100644 index 7f08ec37a1..0000000000 --- a/common/djangoapps/course_about/tests/test_api.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Tests the logical Python API layer of the Course About API. -""" - -import ddt -import json -import unittest - -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 -from xmodule.modulestore.tests.factories import CourseFactory, CourseAboutFactory -from student.tests.factories import UserFactory - - -@ddt.ddt -@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_get_course_details_from_cache(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.assertIsNotNone(resp_data) - - 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.assertIsNotNone(resp_data) diff --git a/common/djangoapps/course_about/tests/test_data.py b/common/djangoapps/course_about/tests/test_data.py deleted file mode 100644 index c9fa14dd31..0000000000 --- a/common/djangoapps/course_about/tests/test_data.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Tests specific to the Data Aggregation Layer of the Course About API. - -""" -import unittest -from datetime import datetime -from django.conf import settings -from nose.tools import raises -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -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 xmodule.modulestore.django import modulestore - - -@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 deleted file mode 100644 index 4db3e8acf1..0000000000 --- a/common/djangoapps/course_about/tests/test_views.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -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 datetime import datetime -from mock import patch -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, CourseAboutFactory -from student.tests.factories import UserFactory -from course_about.serializers import course_image_url -from course_about import api -from course_about.errors import CourseNotFoundError, CourseAboutError -from xmodule.modulestore.django import modulestore - - -@ddt.ddt -@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_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_200_OK) - self.assertIsNotNone(resp_data) - - def test_with_valid_course_id(self): - _resp_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(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_data, status_code = self._get_course_about(self.course.id) - - 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)) - self.assertEqual(status_code, status.HTTP_200_OK) - - 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_data, _status_code = self._get_course_about(self.course.id) - - self.assertIsNotNone(resp_data["start"]) - self.assertIsNotNone(resp_data["end"]) - self.assertIsNotNone(resp_data["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_data, _status_code = self._get_course_about(self.course.id) - self.assertIsNone(resp_data["start"]) - self.assertIsNone(resp_data["end"]) - self.assertIsNone(resp_data["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_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_200_OK) - 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_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(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_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_404_NOT_FOUND) - self.assertIn('An error occurred', resp_data["message"]) - - @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_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) - self.assertIn('An error occurred', resp_data["message"]) - - @override_settings(COURSE_ABOUT_DATA_API='foo') - def test_data_api_config_error(self): - # Retrive the invalid course - resp_data, status_code = self._get_course_about(self.course.id) - self.assertEqual(status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) - self.assertIn('An error occurred', resp_data["message"]) - - def _get_course_about(self, course_id): - """ - helper function to get retrieve course about information. - args course_id (str): course id - """ - resp = self.client.get( - reverse('courseabout', kwargs={"course_id": unicode(course_id)}) - ) - return json.loads(resp.content), resp.status_code diff --git a/common/djangoapps/course_about/urls.py b/common/djangoapps/course_about/urls.py deleted file mode 100644 index 63f9561240..0000000000 --- a/common/djangoapps/course_about/urls.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -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 deleted file mode 100644 index abae554891..0000000000 --- a/common/djangoapps/course_about/views.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -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/lms/envs/aws.py b/lms/envs/aws.py index fc1a9770b1..d2f480ae26 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -566,9 +566,6 @@ COURSE_ABOUT_VISIBILITY_PERMISSION = ENV_TOKENS.get( COURSE_ABOUT_VISIBILITY_PERMISSION ) -#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) # Enrollment API Cache Timeout ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = ENV_TOKENS.get('ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT', 60) diff --git a/lms/envs/common.py b/lms/envs/common.py index 4ceca8457b..ced9bf8cbd 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2360,8 +2360,6 @@ COURSE_CATALOG_VISIBILITY_PERMISSION = 'see_exists' # 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' # Enrollment API Cache Timeout ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = 60 diff --git a/lms/urls.py b/lms/urls.py index 1fecef03d8..d1bf250754 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -75,9 +75,6 @@ urlpatterns = ( # 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')), - # Courseware search endpoints url(r'^search/', include('search.urls')),