Commit 7429a32b by Nimisha Asthagiri

Merge pull request #10799 from edx/mobile/course-api-fields-MA-1661

Update Course About API to include effort and video (MA-1661)
parents 92a436b1 4a530690
...@@ -10,10 +10,10 @@ from django.conf import settings ...@@ -10,10 +10,10 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy, ugettext as _ from django.utils.translation import ugettext_lazy, ugettext as _
from django.core.urlresolvers import resolve from django.core.urlresolvers import resolve
from contentstore.utils import course_image_url
from contentstore.course_group_config import GroupConfiguration from contentstore.course_group_config import GroupConfiguration
from course_modes.models import CourseMode from course_modes.models import CourseMode
from eventtracking import tracker from eventtracking import tracker
from openedx.core.lib.courses import course_image_url
from search.search_engine_base import SearchEngine from search.search_engine_base import SearchEngine
from xmodule.annotator_mixin import html_to_text from xmodule.annotator_mixin import html_to_text
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
......
...@@ -15,6 +15,7 @@ from unittest import skip ...@@ -15,6 +15,7 @@ from unittest import skip
from django.conf import settings from django.conf import settings
from course_modes.models import CourseMode from course_modes.models import CourseMode
from openedx.core.djangoapps.models.course_details import CourseDetails
from xmodule.library_tools import normalize_key_for_search from xmodule.library_tools import normalize_key_for_search
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import SignalHandler, modulestore from xmodule.modulestore.django import SignalHandler, modulestore
...@@ -192,26 +193,6 @@ class MixedWithOptionsTestCase(MixedSplitTestCase): ...@@ -192,26 +193,6 @@ class MixedWithOptionsTestCase(MixedSplitTestCase):
with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred): with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
store.update_item(item, ModuleStoreEnum.UserID.test) store.update_item(item, ModuleStoreEnum.UserID.test)
def update_about_item(self, store, about_key, data):
"""
Update the about item with the new data blob. If data is None, then
delete the about item.
"""
temploc = self.course.id.make_usage_key('about', about_key)
if data is None:
try:
self.delete_item(store, temploc)
# Ignore an attempt to delete an item that doesn't exist
except ValueError:
pass
else:
try:
about_item = store.get_item(temploc)
except ItemNotFoundError:
about_item = store.create_xblock(self.course.runtime, self.course.id, 'about', about_key)
about_item.data = data
store.update_item(about_item, ModuleStoreEnum.UserID.test, allow_not_found=True)
@ddt.ddt @ddt.ddt
class TestCoursewareSearchIndexer(MixedWithOptionsTestCase): class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
...@@ -487,7 +468,9 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase): ...@@ -487,7 +468,9 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
def _test_course_about_store_index(self, store): def _test_course_about_store_index(self, store):
""" Test that informational properties in the about store end up in the course_info index """ """ Test that informational properties in the about store end up in the course_info index """
short_description = "Not just anybody" short_description = "Not just anybody"
self.update_about_item(store, "short_description", short_description) CourseDetails.update_about_item(
self.course, "short_description", short_description, ModuleStoreEnum.UserID.test, store
)
self.reindex_course(store) self.reindex_course(store)
response = self.searcher.search( response = self.searcher.search(
doc_type=CourseAboutSearchIndexer.DISCOVERY_DOCUMENT_TYPE, doc_type=CourseAboutSearchIndexer.DISCOVERY_DOCUMENT_TYPE,
......
...@@ -110,55 +110,6 @@ class ExtraPanelTabTestCase(TestCase): ...@@ -110,55 +110,6 @@ class ExtraPanelTabTestCase(TestCase):
return course return course
@ddt.ddt
class CourseImageTestCase(ModuleStoreTestCase):
"""Tests for course image URLs."""
def verify_url(self, expected_url, actual_url):
"""
Helper method for verifying the URL is as expected.
"""
if not expected_url.startswith("/"):
expected_url = "/" + expected_url
self.assertEquals(expected_url, actual_url)
def test_get_image_url(self):
"""Test image URL formatting."""
course = CourseFactory.create()
self.verify_url(
unicode(course.id.make_asset_key('asset', course.course_image)),
utils.course_image_url(course)
)
def test_non_ascii_image_name(self):
""" Verify that non-ascii image names are cleaned """
course_image = u'before_\N{SNOWMAN}_after.jpg'
course = CourseFactory.create(course_image=course_image)
self.verify_url(
unicode(course.id.make_asset_key('asset', course_image.replace(u'\N{SNOWMAN}', '_'))),
utils.course_image_url(course)
)
def test_spaces_in_image_name(self):
""" Verify that image names with spaces in them are cleaned """
course_image = u'before after.jpg'
course = CourseFactory.create(course_image=u'before after.jpg')
self.verify_url(
unicode(course.id.make_asset_key('asset', course_image.replace(" ", "_"))),
utils.course_image_url(course)
)
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
def test_empty_image_name(self, default_store):
""" Verify that empty image names are cleaned """
course_image = u''
course = CourseFactory.create(course_image=course_image, default_store=default_store)
self.assertEquals(
course_image,
utils.course_image_url(course),
)
class XBlockVisibilityTestCase(ModuleStoreTestCase): class XBlockVisibilityTestCase(ModuleStoreTestCase):
"""Tests for xblock visibility for students.""" """Tests for xblock visibility for students."""
......
...@@ -3,7 +3,6 @@ Common utility functions useful throughout the contentstore ...@@ -3,7 +3,6 @@ Common utility functions useful throughout the contentstore
""" """
import logging import logging
from opaque_keys import InvalidKeyError
import re import re
from datetime import datetime from datetime import datetime
from pytz import UTC from pytz import UTC
...@@ -14,7 +13,6 @@ from django.utils.translation import ugettext as _ ...@@ -14,7 +13,6 @@ from django.utils.translation import ugettext as _
from django_comment_common.models import assign_default_role from django_comment_common.models import assign_default_role
from django_comment_common.utils import seed_permissions_roles from django_comment_common.utils import seed_permissions_roles
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
...@@ -158,16 +156,6 @@ def get_lms_link_for_certificate_web_view(user_id, course_key, mode): ...@@ -158,16 +156,6 @@ def get_lms_link_for_certificate_web_view(user_id, course_key, mode):
) )
def course_image_url(course):
"""Returns the image url for the course."""
try:
loc = StaticContent.compute_location(course.location.course_key, course.course_image)
except InvalidKeyError:
return ''
path = StaticContent.serialize_asset_key_with_slash(loc)
return path
# pylint: disable=invalid-name # pylint: disable=invalid-name
def is_currently_visible_to_students(xblock): def is_currently_visible_to_students(xblock):
""" """
...@@ -314,25 +302,6 @@ def reverse_usage_url(handler_name, usage_key, kwargs=None): ...@@ -314,25 +302,6 @@ def reverse_usage_url(handler_name, usage_key, kwargs=None):
return reverse_url(handler_name, 'usage_key_string', usage_key, kwargs) return reverse_url(handler_name, 'usage_key_string', usage_key, kwargs)
def has_active_web_certificate(course):
"""
Returns True if given course has active web certificate configuration.
If given course has no active web certificate configuration returns False.
Returns None If `CERTIFICATES_HTML_VIEW` is not enabled of course has not enabled
`cert_html_view_enabled` settings.
"""
cert_config = None
if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False) and course.cert_html_view_enabled:
cert_config = False
certificates = getattr(course, 'certificates', {})
configurations = certificates.get('certificates', [])
for config in configurations:
if config.get('is_active'):
cert_config = True
break
return cert_config
def get_user_partition_info(xblock, schemes=None, course=None): def get_user_partition_info(xblock, schemes=None, course=None):
""" """
Retrieve user partition information for an XBlock for display in editors. Retrieve user partition information for an XBlock for display in editors.
......
...@@ -58,16 +58,18 @@ from course_action_state.models import CourseRerunState, CourseRerunUIStateManag ...@@ -58,16 +58,18 @@ from course_action_state.models import CourseRerunState, CourseRerunUIStateManag
from course_creators.views import get_course_creator_status, add_user_with_status_unrequested from course_creators.views import get_course_creator_status, add_user_with_status_unrequested
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from microsite_configuration import microsite from microsite_configuration import microsite
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata from models.settings.course_metadata import CourseMetadata
from models.settings.encoder import CourseSettingsEncoder
from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors from openedx.core.djangoapps.content.course_structures.api.v0 import api, errors
from openedx.core.djangoapps.credit.api import is_credit_course, get_credit_requirements from openedx.core.djangoapps.credit.api import is_credit_course, get_credit_requirements
from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements
from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.utils import get_programs from openedx.core.djangoapps.programs.utils import get_programs
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.lib.course_tabs import CourseTabPluginManager from openedx.core.lib.course_tabs import CourseTabPluginManager
from openedx.core.lib.courses import course_image_url
from openedx.core.lib.js_utils import escape_json_dumps from openedx.core.lib.js_utils import escape_json_dumps
from student import auth from student import auth
from student.auth import has_course_author_access, has_studio_write_access, has_studio_read_access from student.auth import has_course_author_access, has_studio_write_access, has_studio_read_access
...@@ -939,7 +941,7 @@ def settings_handler(request, course_key_string): ...@@ -939,7 +941,7 @@ def settings_handler(request, course_key_string):
'context_course': course_module, 'context_course': course_module,
'course_locator': course_key, 'course_locator': course_key,
'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key), 'lms_link_for_about_page': utils.get_lms_link_for_about_page(course_key),
'course_image_url': utils.course_image_url(course_module), 'course_image_url': course_image_url(course_module),
'details_url': reverse_course_url('settings_handler', course_key), 'details_url': reverse_course_url('settings_handler', course_key),
'about_page_editable': about_page_editable, 'about_page_editable': about_page_editable,
'short_description_editable': short_description_editable, 'short_description_editable': short_description_editable,
......
"""
CourseSettingsEncoder
"""
import datetime
import json
from json.encoder import JSONEncoder
from opaque_keys.edx.locations import Location
from openedx.core.djangoapps.models.course_details import CourseDetails
from xmodule.fields import Date
from .course_grading import CourseGradingModel
class CourseSettingsEncoder(json.JSONEncoder):
"""
Serialize CourseDetails, CourseGradingModel, datetime, and old
Locations
"""
def default(self, obj): # pylint: disable=method-hidden
if isinstance(obj, (CourseDetails, CourseGradingModel)):
return obj.__dict__
elif isinstance(obj, Location):
return obj.dict()
elif isinstance(obj, datetime.datetime):
return Date().to_json(obj)
else:
return JSONEncoder.default(self, obj)
...@@ -31,8 +31,7 @@ define([ ...@@ -31,8 +31,7 @@ define([
entrance_exam_enabled : '', entrance_exam_enabled : '',
entrance_exam_minimum_score_pct: '50', entrance_exam_minimum_score_pct: '50',
license: null, license: null,
language: '', language: ''
has_cert_config: false
}, },
mockSettingsPage = readFixtures('mock/mock-settings-page.underscore'); mockSettingsPage = readFixtures('mock/mock-settings-page.underscore');
......
...@@ -8,8 +8,8 @@ ...@@ -8,8 +8,8 @@
import json import json
from contentstore import utils from contentstore import utils
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from models.settings.encoder import CourseSettingsEncoder
from openedx.core.lib.js_utils import escape_json_dumps from openedx.core.lib.js_utils import escape_json_dumps
from models.settings.course_details import CourseSettingsEncoder
%> %>
<%block name="header_extras"> <%block name="header_extras">
......
"""
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
...@@ -660,7 +660,8 @@ def check_mongo_calls(num_finds=0, num_sends=None): ...@@ -660,7 +660,8 @@ def check_mongo_calls(num_finds=0, num_sends=None):
# This dict represents the attribute keys for a course's 'about' info. # This dict represents the attribute keys for a course's 'about' info.
# Note: The 'video' attribute is intentionally excluded as it must be # Note: The 'video' attribute is intentionally excluded as it must be
# handled separately; its value maps to an alternate key name. # handled separately; its value maps to an alternate key name.
# Reference : cms/djangoapps/models/settings/course_details.py # Reference : from openedx.core.djangoapps.models.course_details.py
ABOUT_ATTRIBUTES = { ABOUT_ATTRIBUTES = {
'effort': "Testing effort", 'effort': "Testing effort",
......
...@@ -39,7 +39,8 @@ from bulk_email.models import ( ...@@ -39,7 +39,8 @@ from bulk_email.models import (
SEND_TO_MYSELF, SEND_TO_ALL, TO_OPTIONS, SEND_TO_MYSELF, SEND_TO_ALL, TO_OPTIONS,
SEND_TO_STAFF, SEND_TO_STAFF,
) )
from courseware.courses import get_course, course_image_url from courseware.courses import get_course
from openedx.core.lib.courses import course_image_url
from student.roles import CourseStaffRole, CourseInstructorRole from student.roles import CourseStaffRole, CourseInstructorRole
from instructor_task.models import InstructorTask from instructor_task.models import InstructorTask
from instructor_task.subtasks import ( from instructor_task.subtasks import (
......
...@@ -15,7 +15,6 @@ from django.utils.translation import ugettext as _ ...@@ -15,7 +15,6 @@ from django.utils.translation import ugettext as _
from django.utils.encoding import smart_str from django.utils.encoding import smart_str
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.courses import course_image_url
from courseware.access import has_access from courseware.access import has_access
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from edxmako.template import Template from edxmako.template import Template
...@@ -23,6 +22,7 @@ from eventtracking import tracker ...@@ -23,6 +22,7 @@ from eventtracking import tracker
from microsite_configuration import microsite from microsite_configuration import microsite
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from openedx.core.lib.courses import course_image_url
from student.models import LinkedInAddToProfileConfiguration from student.models import LinkedInAddToProfileConfiguration
from util import organizations_helpers as organization_api from util import organizations_helpers as organization_api
from util.views import handle_500 from util.views import handle_500
......
...@@ -9,7 +9,9 @@ from django.template import defaultfilters ...@@ -9,7 +9,9 @@ from django.template import defaultfilters
from rest_framework import serializers from rest_framework import serializers
from lms.djangoapps.courseware.courses import course_image_url, get_course_about_section from lms.djangoapps.courseware.courses import get_course_about_section
from openedx.core.lib.courses import course_image_url
from openedx.core.djangoapps.models.course_details import CourseDetails
from xmodule.course_module import DEFAULT_START_DATE from xmodule.course_module import DEFAULT_START_DATE
...@@ -35,6 +37,10 @@ class _CourseApiMediaCollectionSerializer(serializers.Serializer): # pylint: di ...@@ -35,6 +37,10 @@ class _CourseApiMediaCollectionSerializer(serializers.Serializer): # pylint: di
Nested serializer to represent a collection of media objects Nested serializer to represent a collection of media objects
""" """
course_image = _MediaSerializer(source='*', uri_parser=course_image_url) course_image = _MediaSerializer(source='*', uri_parser=course_image_url)
course_video = _MediaSerializer(
source='*',
uri_parser=lambda course: CourseDetails.fetch_video_url(course.id),
)
class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-method class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-method
...@@ -46,14 +52,19 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth ...@@ -46,14 +52,19 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth
name = serializers.CharField(source='display_name_with_default') name = serializers.CharField(source='display_name_with_default')
number = serializers.CharField(source='display_number_with_default') number = serializers.CharField(source='display_number_with_default')
org = serializers.CharField(source='display_org_with_default') org = serializers.CharField(source='display_org_with_default')
description = serializers.SerializerMethodField() short_description = serializers.SerializerMethodField()
effort = serializers.SerializerMethodField()
media = _CourseApiMediaCollectionSerializer(source='*') media = _CourseApiMediaCollectionSerializer(source='*')
start = serializers.DateTimeField() start = serializers.DateTimeField()
start_type = serializers.SerializerMethodField() start_type = serializers.SerializerMethodField()
start_display = serializers.SerializerMethodField() start_display = serializers.SerializerMethodField()
end = serializers.DateTimeField() end = serializers.DateTimeField()
enrollment_start = serializers.DateTimeField() enrollment_start = serializers.DateTimeField()
enrollment_end = serializers.DateTimeField() enrollment_end = serializers.DateTimeField()
blocks_url = serializers.SerializerMethodField() blocks_url = serializers.SerializerMethodField()
def get_start_type(self, course): def get_start_type(self, course):
...@@ -78,9 +89,9 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth ...@@ -78,9 +89,9 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth
else: else:
return None return None
def get_description(self, course): def get_short_description(self, course):
""" """
Get the representation for SerializerMethodField `description` Get the representation for SerializerMethodField `short_description`
""" """
return get_course_about_section(self.context['request'], course, 'short_description').strip() return get_course_about_section(self.context['request'], course, 'short_description').strip()
...@@ -93,3 +104,9 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth ...@@ -93,3 +104,9 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth
urllib.urlencode({'course_id': course.id}), urllib.urlencode({'course_id': course.id}),
]) ])
return self.context['request'].build_absolute_uri(base_url) return self.context['request'].build_absolute_uri(base_url)
def get_effort(self, course):
"""
Get the representation for SerializerMethodField `effort`
"""
return CourseDetails.fetch_effort(course.id)
...@@ -19,44 +19,18 @@ class CourseApiTestMixin(CourseApiFactoryMixin): ...@@ -19,44 +19,18 @@ class CourseApiTestMixin(CourseApiFactoryMixin):
""" """
Establish basic functionality for Course API tests Establish basic functionality for Course API tests
""" """
@classmethod
def setUpClass(cls):
super(CourseApiTestMixin, cls).setUpClass()
cls.request_factory = APIRequestFactory()
maxDiff = 5000 # long enough to show mismatched dicts def verify_course(self, course, course_id=u'edX/toy/2012_Fall'):
expected_course_data = {
'course_id': u'edX/toy/2012_Fall',
'name': u'Toy Course',
'number': u'toy',
'org': u'edX',
'description': u'A course about toys.',
'media': {
'course_image': {
'uri': u'/c4x/edX/toy/asset/just_a_test.jpg',
}
},
'start': u'2015-07-17T12:00:00Z',
'start_type': u'timestamp',
'start_display': u'July 17, 2015',
'end': u'2015-09-19T18:00:00Z',
'enrollment_start': u'2015-06-15T00:00:00Z',
'enrollment_end': u'2015-07-15T00:00:00Z',
'blocks_url': '/api/courses/v1/blocks/?course_id=edX%2Ftoy%2F2012_Fall',
}
def verify_course(self, course, course_id=None):
""" """
Ensure that the returned course is the course we just created Ensure that the returned course is the course we just created
""" """
if course_id is None:
course_id = self.expected_course_data['course_id']
self.assertIsInstance(course, CourseDescriptor) self.assertIsInstance(course, CourseDescriptor)
self.assertEqual(course_id, str(course.id)) self.assertEqual(course_id, str(course.id))
@classmethod
def setUpClass(cls):
super(CourseApiTestMixin, cls).setUpClass()
cls.request_factory = APIRequestFactory()
class CourseDetailTestMixin(CourseApiTestMixin): class CourseDetailTestMixin(CourseApiTestMixin):
""" """
......
...@@ -4,6 +4,7 @@ Test data created by CourseSerializer ...@@ -4,6 +4,7 @@ Test data created by CourseSerializer
from datetime import datetime from datetime import datetime
from openedx.core.djangoapps.models.course_details import CourseDetails
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from rest_framework.request import Request from rest_framework.request import Request
...@@ -18,6 +19,7 @@ class TestCourseSerializerFields(CourseApiFactoryMixin, ModuleStoreTestCase): ...@@ -18,6 +19,7 @@ class TestCourseSerializerFields(CourseApiFactoryMixin, ModuleStoreTestCase):
""" """
Test variations of start_date field responses Test variations of start_date field responses
""" """
maxDiff = 5000 # long enough to show mismatched dicts, in case of error
def setUp(self): def setUp(self):
super(TestCourseSerializerFields, self).setUp() super(TestCourseSerializerFields, self).setUp()
...@@ -35,6 +37,35 @@ class TestCourseSerializerFields(CourseApiFactoryMixin, ModuleStoreTestCase): ...@@ -35,6 +37,35 @@ class TestCourseSerializerFields(CourseApiFactoryMixin, ModuleStoreTestCase):
request.user = user request.user = user
return request return request
def test_basic(self):
expected_data = {
'course_id': u'edX/toy/2012_Fall',
'name': u'Toy Course',
'number': u'toy',
'org': u'edX',
'short_description': u'A course about toys.',
'media': {
'course_image': {
'uri': u'/c4x/edX/toy/asset/just_a_test.jpg',
},
'course_video': {
'uri': u'http://www.youtube.com/watch?v=test_youtube_id',
}
},
'start': u'2015-07-17T12:00:00Z',
'start_type': u'timestamp',
'start_display': u'July 17, 2015',
'end': u'2015-09-19T18:00:00',
'enrollment_start': u'2015-06-15T00:00:00',
'enrollment_end': u'2015-07-15T00:00:00',
'blocks_url': u'http://testserver/api/courses/v1/blocks/?course_id=edX%2Ftoy%2F2012_Fall',
'effort': u'6 hours',
}
course = self.create_course()
CourseDetails.update_about_video(course, 'test_youtube_id', self.staff_user.id) # pylint: disable=no-member
result = CourseSerializer(course, context={'request': self._get_request()}).data
self.assertDictEqual(result, expected_data)
def test_advertised_start(self): def test_advertised_start(self):
course = self.create_course( course = self.create_course(
course=u'custom', course=u'custom',
...@@ -52,16 +83,3 @@ class TestCourseSerializerFields(CourseApiFactoryMixin, ModuleStoreTestCase): ...@@ -52,16 +83,3 @@ class TestCourseSerializerFields(CourseApiFactoryMixin, ModuleStoreTestCase):
self.assertEqual(result['course_id'], u'edX/custom/2012_Fall') self.assertEqual(result['course_id'], u'edX/custom/2012_Fall')
self.assertEqual(result['start_type'], u'empty') self.assertEqual(result['start_type'], u'empty')
self.assertIsNone(result['start_display']) self.assertIsNone(result['start_display'])
def test_description(self):
course = self.create_course()
result = CourseSerializer(course, context={'request': self._get_request()}).data
self.assertEqual(result['description'], u'A course about toys.')
def test_blocks_url(self):
course = self.create_course()
result = CourseSerializer(course, context={'request': self._get_request()}).data
self.assertEqual(
result['blocks_url'],
u'http://testserver/api/courses/v1/blocks/?course_id=edX%2Ftoy%2F2012_Fall'
)
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from rest_framework import serializers from rest_framework import serializers
from courseware.courses import course_image_url from openedx.core.lib.courses import course_image_url
class CourseSerializer(serializers.Serializer): class CourseSerializer(serializers.Serializer):
......
...@@ -6,7 +6,6 @@ from datetime import datetime ...@@ -6,7 +6,6 @@ from datetime import datetime
from collections import defaultdict from collections import defaultdict
from fs.errors import ResourceNotFoundError from fs.errors import ResourceNotFoundError
import logging import logging
import inspect
from path import Path as path from path import Path as path
import pytz import pytz
...@@ -17,7 +16,6 @@ from edxmako.shortcuts import render_to_string ...@@ -17,7 +16,6 @@ from edxmako.shortcuts import render_to_string
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from static_replace import replace_static_urls from static_replace import replace_static_urls
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
...@@ -114,28 +112,6 @@ def get_course_with_access(user, action, course_key, depth=0, check_if_enrolled= ...@@ -114,28 +112,6 @@ def get_course_with_access(user, action, course_key, depth=0, check_if_enrolled=
return course return course
def course_image_url(course):
"""Try to look up the image url for the course. If it's not found,
log an error and return the dead link"""
if course.static_asset_path or modulestore().get_modulestore_type(course.id) == ModuleStoreEnum.Type.xml:
# If we are a static course with the course_image attribute
# set different than the default, return that path so that
# courses can use custom course image paths, otherwise just
# return the default static path.
url = '/static/' + (course.static_asset_path or getattr(course, 'data_dir', ''))
if hasattr(course, 'course_image') and course.course_image != course.fields['course_image'].default:
url += '/' + course.course_image
else:
url += '/images/course_image.jpg'
elif not course.course_image:
# if course_image is empty, use the default image url from settings
url = settings.STATIC_URL + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL
else:
loc = StaticContent.compute_location(course.id, course.course_image)
url = StaticContent.serialize_asset_key_with_slash(loc)
return url
def find_file(filesystem, dirs, filename): def find_file(filesystem, dirs, filename):
""" """
Looks for a filename in a list of dirs on a filesystem, in the specified order. Looks for a filename in a list of dirs on a filesystem, in the specified order.
......
...@@ -14,7 +14,7 @@ from django.test.client import RequestFactory ...@@ -14,7 +14,7 @@ from django.test.client import RequestFactory
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from courseware.courses import ( from courseware.courses import (
get_course_by_id, get_cms_course_link, course_image_url, get_course_by_id, get_cms_course_link,
get_course_info_section, get_course_about_section, get_cms_block_link get_course_info_section, get_course_about_section, get_cms_block_link
) )
...@@ -23,6 +23,7 @@ from courseware.module_render import get_module_for_descriptor ...@@ -23,6 +23,7 @@ from courseware.module_render import get_module_for_descriptor
from courseware.tests.helpers import get_request_for_user from courseware.tests.helpers import get_request_for_user
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException from lms.djangoapps.courseware.courseware_access_exception import CoursewareAccessException
from openedx.core.lib.courses import course_image_url
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore.django import _get_modulestore_branch_setting, modulestore from xmodule.modulestore.django import _get_modulestore_branch_setting, modulestore
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
......
...@@ -29,7 +29,7 @@ from xblock.fragment import Fragment ...@@ -29,7 +29,7 @@ from xblock.fragment import Fragment
from capa.tests.response_xml_factory import OptionResponseXMLFactory from capa.tests.response_xml_factory import OptionResponseXMLFactory
from course_modes.models import CourseMode from course_modes.models import CourseMode
from courseware import module_render as render from courseware import module_render as render
from courseware.courses import get_course_with_access, course_image_url, get_course_info_section from courseware.courses import get_course_with_access, get_course_info_section
from courseware.field_overrides import OverrideFieldData from courseware.field_overrides import OverrideFieldData
from courseware.model_data import FieldDataCache from courseware.model_data import FieldDataCache
from courseware.module_render import hash_resource, get_module_for_descriptor from courseware.module_render import hash_resource, get_module_for_descriptor
...@@ -39,6 +39,7 @@ from courseware.tests.tests import LoginEnrollmentTestCase ...@@ -39,6 +39,7 @@ from courseware.tests.tests import LoginEnrollmentTestCase
from courseware.tests.test_submitting_problems import TestSubmittingProblems from courseware.tests.test_submitting_problems import TestSubmittingProblems
from lms.djangoapps.lms_xblock.runtime import quote_slashes from lms.djangoapps.lms_xblock.runtime import quote_slashes
from lms.djangoapps.lms_xblock.field_data import LmsFieldData from lms.djangoapps.lms_xblock.field_data import LmsFieldData
from openedx.core.lib.courses import course_image_url
from student.models import anonymous_id_for_user from student.models import anonymous_id_for_user
from xmodule.modulestore.tests.django_utils import ( from xmodule.modulestore.tests.django_utils import (
TEST_DATA_MIXED_TOY_MODULESTORE, TEST_DATA_MIXED_TOY_MODULESTORE,
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
<%! <%!
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.courses import course_image_url, get_course_about_section from courseware.courses import get_course_about_section
from openedx.core.lib.courses import course_image_url
%> %>
<%page args="course" /> <%page args="course" />
<article class="course" id="${course.id | h}" role="region" aria-label="${get_course_about_section(request, course, 'title')}"> <article class="course" id="${course.id | h}" role="region" aria-label="${get_course_about_section(request, course, 'title')}">
......
...@@ -3,9 +3,10 @@ ...@@ -3,9 +3,10 @@
from microsite_configuration import microsite from microsite_configuration import microsite
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.courses import course_image_url, get_course_about_section from courseware.courses import get_course_about_section
from django.conf import settings from django.conf import settings
from edxmako.shortcuts import marketing_link from edxmako.shortcuts import marketing_link
from openedx.core.lib.courses import course_image_url
%> %>
<%inherit file="../main.html" /> <%inherit file="../main.html" />
......
...@@ -3,9 +3,10 @@ ...@@ -3,9 +3,10 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ungettext from django.utils.translation import ungettext
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.courses import course_image_url, get_course_about_section, get_course_by_id from courseware.courses import get_course_about_section, get_course_by_id
from markupsafe import escape from markupsafe import escape
from microsite_configuration import microsite from microsite_configuration import microsite
from openedx.core.lib.courses import course_image_url
%> %>
<%block name="billing_details_highlight"> <%block name="billing_details_highlight">
......
<%! <%!
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.courses import course_image_url, get_course_about_section from courseware.courses import get_course_about_section
from openedx.core.lib.courses import course_image_url
%> %>
<%inherit file="../main.html" /> <%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
......
<%! <%!
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.courses import course_image_url, get_course_about_section from courseware.courses import get_course_about_section
from openedx.core.lib.courses import course_image_url
%> %>
<%inherit file="../main.html" /> <%inherit file="../main.html" />
<%namespace name='static' file='/static_content.html'/> <%namespace name='static' file='/static_content.html'/>
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
<%block name="review_highlight">class="active"</%block> <%block name="review_highlight">class="active"</%block>
<%! <%!
from courseware.courses import course_image_url, get_course_about_section from courseware.courses import get_course_about_section
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from edxmako.shortcuts import marketing_link from edxmako.shortcuts import marketing_link
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.translation import ungettext from django.utils.translation import ungettext
from openedx.core.lib.courses import course_image_url
%> %>
<section class="wrapper confirm-enrollment shopping-cart cart-view"> <section class="wrapper confirm-enrollment shopping-cart cart-view">
......
...@@ -97,7 +97,7 @@ class CourseOverview(TimeStampedModel): ...@@ -97,7 +97,7 @@ class CourseOverview(TimeStampedModel):
CourseOverview: overview extracted from the given course CourseOverview: overview extracted from the given course
""" """
from lms.djangoapps.certificates.api import get_active_web_certificate from lms.djangoapps.certificates.api import get_active_web_certificate
from lms.djangoapps.courseware.courses import course_image_url from openedx.core.lib.courses import course_image_url
# Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806. # Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806.
# If the course has a malformed grading policy such that # If the course has a malformed grading policy such that
......
...@@ -11,7 +11,7 @@ import pytz ...@@ -11,7 +11,7 @@ import pytz
from django.utils import timezone from django.utils import timezone
from lms.djangoapps.certificates.api import get_active_web_certificate from lms.djangoapps.certificates.api import get_active_web_certificate
from lms.djangoapps.courseware.courses import course_image_url from openedx.core.lib.courses import course_image_url
from xmodule.course_metadata_utils import DEFAULT_START_DATE from xmodule.course_metadata_utils import DEFAULT_START_DATE
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
......
"""
Tests for CourseDetails
"""
import datetime
from django.utils.timezone import UTC
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.models.course_details import CourseDetails
class CourseDetailsTestCase(ModuleStoreTestCase):
"""
Tests the first course settings page (course dates, overview, etc.).
"""
def setUp(self):
super(CourseDetailsTestCase, self).setUp()
self.course = CourseFactory.create()
def test_virgin_fetch(self):
details = CourseDetails.fetch(self.course.id)
self.assertEqual(details.org, self.course.location.org, "Org not copied into")
self.assertEqual(details.course_id, self.course.location.course, "Course_id not copied into")
self.assertEqual(details.run, self.course.location.name, "Course name not copied into")
self.assertEqual(details.course_image_name, self.course.course_image)
self.assertIsNotNone(details.start_date.tzinfo)
self.assertIsNone(details.end_date, "end date somehow initialized " + str(details.end_date))
self.assertIsNone(
details.enrollment_start, "enrollment_start date somehow initialized " + str(details.enrollment_start)
)
self.assertIsNone(
details.enrollment_end, "enrollment_end date somehow initialized " + str(details.enrollment_end)
)
self.assertIsNone(details.syllabus, "syllabus somehow initialized" + str(details.syllabus))
self.assertIsNone(details.intro_video, "intro_video somehow initialized" + str(details.intro_video))
self.assertIsNone(details.effort, "effort somehow initialized" + str(details.effort))
self.assertIsNone(details.language, "language somehow initialized" + str(details.language))
self.assertFalse(details.self_paced)
def test_update_and_fetch(self):
SelfPacedConfiguration(enabled=True).save()
jsondetails = CourseDetails.fetch(self.course.id)
jsondetails.syllabus = "<a href='foo'>bar</a>"
# encode - decode to convert date fields and other data which changes form
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).syllabus,
jsondetails.syllabus, "After set syllabus"
)
jsondetails.short_description = "Short Description"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).short_description,
jsondetails.short_description, "After set short_description"
)
jsondetails.overview = "Overview"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).overview,
jsondetails.overview, "After set overview"
)
jsondetails.intro_video = "intro_video"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).intro_video,
jsondetails.intro_video, "After set intro_video"
)
jsondetails.effort = "effort"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).effort,
jsondetails.effort, "After set effort"
)
jsondetails.self_paced = True
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).self_paced,
jsondetails.self_paced
)
jsondetails.start_date = datetime.datetime(2010, 10, 1, 0, tzinfo=UTC())
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).start_date,
jsondetails.start_date
)
jsondetails.end_date = datetime.datetime(2011, 10, 1, 0, tzinfo=UTC())
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).end_date,
jsondetails.end_date
)
jsondetails.course_image_name = "an_image.jpg"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).course_image_name,
jsondetails.course_image_name
)
jsondetails.language = "hr"
self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).language,
jsondetails.language
)
def test_toggle_pacing_during_course_run(self):
SelfPacedConfiguration(enabled=True).save()
self.course.start = datetime.datetime.now()
self.store.update_item(self.course, self.user.id)
details = CourseDetails.fetch(self.course.id)
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
updated_details = CourseDetails.update_from_json(
self.course.id,
dict(details.__dict__, self_paced=True),
self.user
)
self.assertFalse(updated_details.self_paced)
def test_fetch_effort(self):
effort_value = 'test_hours_of_effort'
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
CourseDetails.update_about_item(self.course, 'effort', effort_value, self.user.id)
self.assertEqual(CourseDetails.fetch_effort(self.course.id), effort_value)
def test_fetch_video(self):
video_value = 'test_video_id'
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred, self.course.id):
CourseDetails.update_about_video(self.course, video_value, self.user.id)
self.assertEqual(CourseDetails.fetch_youtube_video_id(self.course.id), video_value)
video_url = CourseDetails.fetch_video_url(self.course.id)
self.assertRegexpMatches(video_url, r'http://.*{}'.format(video_value))
"""
Common utility functions related to courses.
"""
from django.conf import settings
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore import ModuleStoreEnum
def course_image_url(course):
"""Try to look up the image url for the course. If it's not found,
log an error and return the dead link"""
if course.static_asset_path or modulestore().get_modulestore_type(course.id) == ModuleStoreEnum.Type.xml:
# If we are a static course with the course_image attribute
# set different than the default, return that path so that
# courses can use custom course image paths, otherwise just
# return the default static path.
url = '/static/' + (course.static_asset_path or getattr(course, 'data_dir', ''))
if hasattr(course, 'course_image') and course.course_image != course.fields['course_image'].default:
url += '/' + course.course_image
else:
url += '/images/course_image.jpg'
elif not course.course_image:
# if course_image is empty, use the default image url from settings
url = settings.STATIC_URL + settings.DEFAULT_COURSE_ABOUT_IMAGE_URL
else:
loc = StaticContent.compute_location(course.id, course.course_image)
url = StaticContent.serialize_asset_key_with_slash(loc)
return url
"""
Tests for functionality in openedx/core/lib/courses.py.
"""
import ddt
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from ..courses import course_image_url
@ddt.ddt
class CourseImageTestCase(ModuleStoreTestCase):
"""Tests for course image URLs."""
def verify_url(self, expected_url, actual_url):
"""
Helper method for verifying the URL is as expected.
"""
if not expected_url.startswith("/"):
expected_url = "/" + expected_url
self.assertEquals(expected_url, actual_url)
def test_get_image_url(self):
"""Test image URL formatting."""
course = CourseFactory.create()
self.verify_url(
unicode(course.id.make_asset_key('asset', course.course_image)),
course_image_url(course)
)
def test_non_ascii_image_name(self):
""" Verify that non-ascii image names are cleaned """
course_image = u'before_\N{SNOWMAN}_after.jpg'
course = CourseFactory.create(course_image=course_image)
self.verify_url(
unicode(course.id.make_asset_key('asset', course_image.replace(u'\N{SNOWMAN}', '_'))),
course_image_url(course)
)
def test_spaces_in_image_name(self):
""" Verify that image names with spaces in them are cleaned """
course_image = u'before after.jpg'
course = CourseFactory.create(course_image=u'before after.jpg')
self.verify_url(
unicode(course.id.make_asset_key('asset', course_image.replace(" ", "_"))),
course_image_url(course)
)
@ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo)
def test_empty_image_name(self, default_store):
""" Verify that empty image names are cleaned """
course_image = u''
course = CourseFactory.create(course_image=course_image, default_store=default_store)
self.assertEquals(
course_image,
course_image_url(course),
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment