Commit d6ced072 by Nimisha Asthagiri

Refactor CourseDetails

parent d170c92f
...@@ -2,63 +2,44 @@ ...@@ -2,63 +2,44 @@
Tests for Studio Course Settings. Tests for Studio Course Settings.
""" """
import datetime import datetime
import ddt
import json import json
import copy import copy
import mock import mock
from mock import patch from mock import patch
import unittest import unittest
from django.conf import settings
from django.utils.timezone import UTC from django.utils.timezone import UTC
from django.test.utils import override_settings from django.test.utils import override_settings
from django.conf import settings
from models.settings.course_details import (CourseDetails, CourseSettingsEncoder)
from models.settings.course_grading import CourseGradingModel
from contentstore.utils import reverse_course_url, reverse_usage_url from contentstore.utils import reverse_course_url, reverse_usage_url
from xmodule.modulestore.tests.factories import CourseFactory from contentstore.views.component import ADVANCED_COMPONENT_POLICY_KEY
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
from models.settings.encoder import CourseSettingsEncoder
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.djangoapps.models.course_details import CourseDetails
from student.roles import CourseInstructorRole from student.roles import CourseInstructorRole
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from models.settings.course_metadata import CourseMetadata
from xmodule.fields import Date from xmodule.fields import Date
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.tabs import InvalidTabsException from xmodule.tabs import InvalidTabsException
from util.milestones_helpers import seed_milestone_relationship_types
from .utils import CourseTestCase from .utils import CourseTestCase
from xmodule.modulestore.django import modulestore
from contentstore.views.component import ADVANCED_COMPONENT_POLICY_KEY
import ddt
from xmodule.modulestore import ModuleStoreEnum
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from util.milestones_helpers import seed_milestone_relationship_types
def get_url(course_id, handler_name='settings_handler'): def get_url(course_id, handler_name='settings_handler'):
return reverse_course_url(handler_name, course_id) return reverse_course_url(handler_name, course_id)
class CourseDetailsTestCase(CourseTestCase): class CourseSettingsEncoderTest(CourseTestCase):
""" """
Tests the first course settings page (course dates, overview, etc.). Tests for CourseSettingsEncoder.
""" """
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.assertIsNone(details.has_cert_config)
self.assertFalse(details.self_paced)
def test_encoder(self): def test_encoder(self):
details = CourseDetails.fetch(self.course.id) details = CourseDetails.fetch(self.course.id)
jsondetails = json.dumps(details, cls=CourseSettingsEncoder) jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
...@@ -87,60 +68,163 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -87,60 +68,163 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertEquals(1, jsondetails['number']) self.assertEquals(1, jsondetails['number'])
self.assertEqual(jsondetails['string'], 'string') self.assertEqual(jsondetails['string'], 'string')
@ddt.ddt
class CourseDetailsViewTest(CourseTestCase):
"""
Tests for modifying content on the first course settings page (course dates, overview, etc.).
"""
def setUp(self):
super(CourseDetailsViewTest, self).setUp()
def alter_field(self, url, details, field, val):
"""
Change the one field to the given value and then invoke the update post to see if it worked.
"""
setattr(details, field, val)
# Need to partially serialize payload b/c the mock doesn't handle it correctly
payload = copy.copy(details.__dict__)
payload['start_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.start_date)
payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date)
payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start)
payload['enrollment_end'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_end)
resp = self.client.ajax_post(url, payload)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
@staticmethod
def convert_datetime_to_iso(datetime_obj):
"""
Use the xblock serializer to convert the datetime
"""
return Date().to_json(datetime_obj)
def test_update_and_fetch(self): def test_update_and_fetch(self):
SelfPacedConfiguration(enabled=True).save() SelfPacedConfiguration(enabled=True).save()
jsondetails = CourseDetails.fetch(self.course.id) details = CourseDetails.fetch(self.course.id)
jsondetails.syllabus = "<a href='foo'>bar</a>"
# encode - decode to convert date fields and other data which changes form # resp s/b json from here on
self.assertEqual( url = get_url(self.course.id)
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).syllabus, resp = self.client.get_json(url)
jsondetails.syllabus, "After set syllabus" self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
)
jsondetails.short_description = "Short Description" utc = UTC()
self.assertEqual( self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 12, 1, 30, tzinfo=utc))
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).short_description, self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 1, 13, 30, tzinfo=utc))
jsondetails.short_description, "After set short_description" self.alter_field(url, details, 'end_date', datetime.datetime(2013, 2, 12, 1, 30, tzinfo=utc))
) self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012, 10, 12, 1, 30, tzinfo=utc))
jsondetails.overview = "Overview"
self.assertEqual( self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012, 11, 15, 1, 30, tzinfo=utc))
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).overview, self.alter_field(url, details, 'short_description', "Short Description")
jsondetails.overview, "After set overview" self.alter_field(url, details, 'overview', "Overview")
) self.alter_field(url, details, 'intro_video', "intro_video")
jsondetails.intro_video = "intro_video" self.alter_field(url, details, 'effort', "effort")
self.assertEqual( self.alter_field(url, details, 'course_image_name', "course_image_name")
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).intro_video, self.alter_field(url, details, 'language', "en")
jsondetails.intro_video, "After set intro_video" self.alter_field(url, details, 'self_paced', "true")
)
jsondetails.effort = "effort" def compare_details_with_encoding(self, encoded, details, context):
self.assertEqual( """
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).effort, compare all of the fields of the before and after dicts
jsondetails.effort, "After set effort" """
) self.compare_date_fields(details, encoded, context, 'start_date')
jsondetails.self_paced = True self.compare_date_fields(details, encoded, context, 'end_date')
self.assertEqual( self.compare_date_fields(details, encoded, context, 'enrollment_start')
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).self_paced, self.compare_date_fields(details, encoded, context, 'enrollment_end')
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( self.assertEqual(
CourseDetails.update_from_json(self.course.id, jsondetails.__dict__, self.user).language, details['short_description'], encoded['short_description'], context + " short_description not =="
jsondetails.language
) )
self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==")
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
self.assertEqual(details['course_image_name'], encoded['course_image_name'], context + " images not ==")
self.assertEqual(details['language'], encoded['language'], context + " languages not ==")
def compare_date_fields(self, details, encoded, context, field):
"""
Compare the given date fields between the before and after doing json deserialization
"""
if details[field] is not None:
date = Date()
if field in encoded and encoded[field] is not None:
dt1 = date.from_json(encoded[field])
dt2 = details[field]
self.assertEqual(dt1, dt2, msg="{} != {} at {}".format(dt1, dt2, context))
else:
self.fail(field + " missing from encoded but in details at " + context)
elif field in encoded and encoded[field] is not None:
self.fail(field + " included in encoding but missing from details at " + context)
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_pre_requisite_course_list_present(self):
seed_milestone_relationship_types()
settings_details_url = get_url(self.course.id)
response = self.client.get_html(settings_details_url)
self.assertContains(response, "Prerequisite Course")
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_pre_requisite_course_update_and_fetch(self):
seed_milestone_relationship_types()
url = get_url(self.course.id)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
# assert pre_requisite_courses is initialized
self.assertEqual([], course_detail_json['pre_requisite_courses'])
# update pre requisite courses with a new course keys
pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
pre_requisite_course2 = CourseFactory.create(org='edX', course='902', run='test_run')
pre_requisite_course_keys = [unicode(pre_requisite_course.id), unicode(pre_requisite_course2.id)]
course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
self.client.ajax_post(url, course_detail_json)
# fetch updated course to assert pre_requisite_courses has new values
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
self.assertEqual(pre_requisite_course_keys, course_detail_json['pre_requisite_courses'])
# remove pre requisite course
course_detail_json['pre_requisite_courses'] = []
self.client.ajax_post(url, course_detail_json)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
self.assertEqual([], course_detail_json['pre_requisite_courses'])
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_invalid_pre_requisite_course(self):
seed_milestone_relationship_types()
url = get_url(self.course.id)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
# update pre requisite courses one valid and one invalid key
pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
pre_requisite_course_keys = [unicode(pre_requisite_course.id), 'invalid_key']
course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
response = self.client.ajax_post(url, course_detail_json)
self.assertEqual(400, response.status_code)
@ddt.data(
(False, False, False),
(True, False, True),
(False, True, False),
(True, True, True),
)
def test_visibility_of_entrance_exam_section(self, feature_flags):
"""
Tests entrance exam section is available if ENTRANCE_EXAMS feature is enabled no matter any other
feature is enabled or disabled i.e ENABLE_MKTG_SITE.
"""
with patch.dict("django.conf.settings.FEATURES", {
'ENTRANCE_EXAMS': feature_flags[0],
'ENABLE_MKTG_SITE': feature_flags[1]
}):
course_details_url = get_url(self.course.id)
resp = self.client.get_html(course_details_url)
self.assertEqual(
feature_flags[2],
'<h3 id="heading-entrance-exam">' in resp.content
)
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'}) @override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
def test_marketing_site_fetch(self): def test_marketing_site_fetch(self):
...@@ -296,175 +380,6 @@ class CourseDetailsTestCase(CourseTestCase): ...@@ -296,175 +380,6 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertContains(response, "Course Introduction Video") self.assertContains(response, "Course Introduction Video")
self.assertContains(response, "Requirements") self.assertContains(response, "Requirements")
def test_toggle_pacing_during_course_run(self):
SelfPacedConfiguration(enabled=True).save()
self.course.start = datetime.datetime.now()
modulestore().update_item(self.course, self.user.id)
details = CourseDetails.fetch(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)
@ddt.ddt
class CourseDetailsViewTest(CourseTestCase):
"""
Tests for modifying content on the first course settings page (course dates, overview, etc.).
"""
def setUp(self):
super(CourseDetailsViewTest, self).setUp()
def alter_field(self, url, details, field, val):
"""
Change the one field to the given value and then invoke the update post to see if it worked.
"""
setattr(details, field, val)
# Need to partially serialize payload b/c the mock doesn't handle it correctly
payload = copy.copy(details.__dict__)
payload['start_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.start_date)
payload['end_date'] = CourseDetailsViewTest.convert_datetime_to_iso(details.end_date)
payload['enrollment_start'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_start)
payload['enrollment_end'] = CourseDetailsViewTest.convert_datetime_to_iso(details.enrollment_end)
resp = self.client.ajax_post(url, payload)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, field + str(val))
@staticmethod
def convert_datetime_to_iso(datetime_obj):
"""
Use the xblock serializer to convert the datetime
"""
return Date().to_json(datetime_obj)
def test_update_and_fetch(self):
SelfPacedConfiguration(enabled=True).save()
details = CourseDetails.fetch(self.course.id)
# resp s/b json from here on
url = get_url(self.course.id)
resp = self.client.get_json(url)
self.compare_details_with_encoding(json.loads(resp.content), details.__dict__, "virgin get")
utc = UTC()
self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'start_date', datetime.datetime(2012, 11, 1, 13, 30, tzinfo=utc))
self.alter_field(url, details, 'end_date', datetime.datetime(2013, 2, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'enrollment_start', datetime.datetime(2012, 10, 12, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'enrollment_end', datetime.datetime(2012, 11, 15, 1, 30, tzinfo=utc))
self.alter_field(url, details, 'short_description', "Short Description")
self.alter_field(url, details, 'overview', "Overview")
self.alter_field(url, details, 'intro_video', "intro_video")
self.alter_field(url, details, 'effort', "effort")
self.alter_field(url, details, 'course_image_name', "course_image_name")
self.alter_field(url, details, 'language', "en")
self.alter_field(url, details, 'self_paced', "true")
def compare_details_with_encoding(self, encoded, details, context):
"""
compare all of the fields of the before and after dicts
"""
self.compare_date_fields(details, encoded, context, 'start_date')
self.compare_date_fields(details, encoded, context, 'end_date')
self.compare_date_fields(details, encoded, context, 'enrollment_start')
self.compare_date_fields(details, encoded, context, 'enrollment_end')
self.assertEqual(details['short_description'], encoded['short_description'], context + " short_description not ==")
self.assertEqual(details['overview'], encoded['overview'], context + " overviews not ==")
self.assertEqual(details['intro_video'], encoded.get('intro_video', None), context + " intro_video not ==")
self.assertEqual(details['effort'], encoded['effort'], context + " efforts not ==")
self.assertEqual(details['course_image_name'], encoded['course_image_name'], context + " images not ==")
self.assertEqual(details['language'], encoded['language'], context + " languages not ==")
def compare_date_fields(self, details, encoded, context, field):
"""
Compare the given date fields between the before and after doing json deserialization
"""
if details[field] is not None:
date = Date()
if field in encoded and encoded[field] is not None:
dt1 = date.from_json(encoded[field])
dt2 = details[field]
self.assertEqual(dt1, dt2, msg="{} != {} at {}".format(dt1, dt2, context))
else:
self.fail(field + " missing from encoded but in details at " + context)
elif field in encoded and encoded[field] is not None:
self.fail(field + " included in encoding but missing from details at " + context)
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_pre_requisite_course_list_present(self):
seed_milestone_relationship_types()
settings_details_url = get_url(self.course.id)
response = self.client.get_html(settings_details_url)
self.assertContains(response, "Prerequisite Course")
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_pre_requisite_course_update_and_fetch(self):
seed_milestone_relationship_types()
url = get_url(self.course.id)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
# assert pre_requisite_courses is initialized
self.assertEqual([], course_detail_json['pre_requisite_courses'])
# update pre requisite courses with a new course keys
pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
pre_requisite_course2 = CourseFactory.create(org='edX', course='902', run='test_run')
pre_requisite_course_keys = [unicode(pre_requisite_course.id), unicode(pre_requisite_course2.id)]
course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
self.client.ajax_post(url, course_detail_json)
# fetch updated course to assert pre_requisite_courses has new values
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
self.assertEqual(pre_requisite_course_keys, course_detail_json['pre_requisite_courses'])
# remove pre requisite course
course_detail_json['pre_requisite_courses'] = []
self.client.ajax_post(url, course_detail_json)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
self.assertEqual([], course_detail_json['pre_requisite_courses'])
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_PREREQUISITE_COURSES': True, 'MILESTONES_APP': True})
def test_invalid_pre_requisite_course(self):
seed_milestone_relationship_types()
url = get_url(self.course.id)
resp = self.client.get_json(url)
course_detail_json = json.loads(resp.content)
# update pre requisite courses one valid and one invalid key
pre_requisite_course = CourseFactory.create(org='edX', course='900', run='test_run')
pre_requisite_course_keys = [unicode(pre_requisite_course.id), 'invalid_key']
course_detail_json['pre_requisite_courses'] = pre_requisite_course_keys
response = self.client.ajax_post(url, course_detail_json)
self.assertEqual(400, response.status_code)
@ddt.data(
(False, False, False),
(True, False, True),
(False, True, False),
(True, True, True),
)
def test_visibility_of_entrance_exam_section(self, feature_flags):
"""
Tests entrance exam section is available if ENTRANCE_EXAMS feature is enabled no matter any other
feature is enabled or disabled i.e ENABLE_MKTG_SITE.
"""
with patch.dict("django.conf.settings.FEATURES", {
'ENTRANCE_EXAMS': feature_flags[0],
'ENABLE_MKTG_SITE': feature_flags[1]
}):
course_details_url = get_url(self.course.id)
resp = self.client.get_html(course_details_url)
self.assertEqual(
feature_flags[2],
'<h3 id="heading-entrance-exam">' in resp.content
)
@ddt.ddt @ddt.ddt
class CourseGradingTest(CourseTestCase): class CourseGradingTest(CourseTestCase):
...@@ -1068,7 +983,7 @@ class CourseMetadataEditingTest(CourseTestCase): ...@@ -1068,7 +983,7 @@ class CourseMetadataEditingTest(CourseTestCase):
self.assertEqual(tab_list, course.tabs) self.assertEqual(tab_list, course.tabs)
@override_settings(FEATURES={'CERTIFICATES_HTML_VIEW': True}) @override_settings(FEATURES={'CERTIFICATES_HTML_VIEW': True})
def test_web_view_certifcate_configuration_settings(self): def test_web_view_certificate_configuration_settings(self):
""" """
Test that has_cert_config is updated based on cert_html_view_enabled setting. Test that has_cert_config is updated based on cert_html_view_enabled setting.
""" """
......
...@@ -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,
......
...@@ -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
...@@ -304,25 +302,6 @@ def reverse_usage_url(handler_name, usage_key, kwargs=None): ...@@ -304,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,12 +58,13 @@ from course_action_state.models import CourseRerunState, CourseRerunUIStateManag ...@@ -58,12 +58,13 @@ 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
......
"""
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)
...@@ -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">
......
...@@ -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",
......
"""
CourseDetails
"""
import re import re
import logging import logging
import datetime
import json
from json.encoder import JSONEncoder
from django.conf import settings from django.conf import settings
from opaque_keys.edx.locations import Location from xmodule.fields import Date
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from contentstore.utils import has_active_web_certificate
from models.settings import course_grading
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
from openedx.core.lib.courses import course_image_url from openedx.core.lib.courses import course_image_url
from xmodule.fields import Date
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
# This list represents the attribute keys for a course's 'about' info. # This list 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.
...@@ -30,8 +28,12 @@ ABOUT_ATTRIBUTES = [ ...@@ -30,8 +28,12 @@ ABOUT_ATTRIBUTES = [
class CourseDetails(object): class CourseDetails(object):
"""
An interface for extracting course information from the modulestore.
"""
def __init__(self, org, course_id, run): def __init__(self, org, course_id, run):
# still need these for now b/c the client's screen shows these 3 fields # still need these for now b/c the client's screen shows these 3
# fields
self.org = org self.org = org
self.course_id = course_id self.course_id = course_id
self.run = run self.run = run
...@@ -44,7 +46,7 @@ class CourseDetails(object): ...@@ -44,7 +46,7 @@ class CourseDetails(object):
self.short_description = "" self.short_description = ""
self.overview = "" # html to render as the overview self.overview = "" # html to render as the overview
self.intro_video = None # a video pointer self.intro_video = None # a video pointer
self.effort = None # int hours/week self.effort = None # hours/week
self.license = "all-rights-reserved" # default course license is all rights reserved self.license = "all-rights-reserved" # default course license is all rights reserved
self.course_image_name = "" self.course_image_name = ""
self.course_image_asset_path = "" # URL of the course image self.course_image_asset_path = "" # URL of the course image
...@@ -73,7 +75,8 @@ class CourseDetails(object): ...@@ -73,7 +75,8 @@ class CourseDetails(object):
@classmethod @classmethod
def fetch(cls, course_key): def fetch(cls, course_key):
""" """
Fetch the course details for the given course from persistence and return a CourseDetails model. Fetch the course details for the given course from persistence
and return a CourseDetails model.
""" """
descriptor = modulestore().get_course(course_key) descriptor = modulestore().get_course(course_key)
course_details = cls(course_key.org, course_key.course, course_key.run) course_details = cls(course_key.org, course_key.course, course_key.run)
...@@ -86,33 +89,57 @@ class CourseDetails(object): ...@@ -86,33 +89,57 @@ class CourseDetails(object):
course_details.course_image_name = descriptor.course_image course_details.course_image_name = descriptor.course_image
course_details.course_image_asset_path = course_image_url(descriptor) course_details.course_image_asset_path = course_image_url(descriptor)
course_details.language = descriptor.language course_details.language = descriptor.language
course_details.self_paced = descriptor.self_paced
# Default course license is "All Rights Reserved" # Default course license is "All Rights Reserved"
course_details.license = getattr(descriptor, "license", "all-rights-reserved") course_details.license = getattr(descriptor, "license", "all-rights-reserved")
course_details.has_cert_config = has_active_web_certificate(descriptor) course_details.has_cert_config = has_active_web_certificate(descriptor)
course_details.self_paced = descriptor.self_paced course_details.intro_video = cls.fetch_youtube_video_id(course_key)
for attribute in ABOUT_ATTRIBUTES: for attribute in ABOUT_ATTRIBUTES:
value = cls._fetch_about_attribute(course_key, attribute) value = cls._fetch_about_attribute(course_key, attribute)
if value is not None: if value is not None:
setattr(course_details, attribute, value) setattr(course_details, attribute, value)
return course_details
@classmethod
def fetch_youtube_video_id(cls, course_key):
"""
Returns the course about video ID.
"""
raw_video = cls._fetch_about_attribute(course_key, 'video') raw_video = cls._fetch_about_attribute(course_key, 'video')
if raw_video: if raw_video:
course_details.intro_video = CourseDetails.parse_video_tag(raw_video) return cls.parse_video_tag(raw_video)
return course_details @classmethod
def fetch_video_url(cls, course_key):
"""
Returns the course about video URL.
"""
video_id = cls.fetch_youtube_video_id(course_key)
if video_id:
return "http://www.youtube.com/watch?v={0}".format(video_id)
@classmethod
def fetch_effort(cls, course_key):
"""
Returns the hours per week of effort for the course.
"""
return cls._fetch_about_attribute(course_key, 'effort')
@classmethod @classmethod
def update_about_item(cls, course_key, about_key, data, course, user): def update_about_item(cls, course, about_key, data, user_id, store=None):
""" """
Update the about item with the new data blob. If data is None, then Update the about item with the new data blob. If data is None,
delete the about item. then delete the about item.
""" """
temploc = course_key.make_usage_key('about', about_key) temploc = course.id.make_usage_key('about', about_key)
store = modulestore() store = store or modulestore()
if data is None: if data is None:
try: try:
store.delete_item(temploc, user.id) store.delete_item(temploc, user_id)
# Ignore an attempt to delete an item that doesn't exist # Ignore an attempt to delete an item that doesn't exist
except ValueError: except ValueError:
pass pass
...@@ -122,10 +149,18 @@ class CourseDetails(object): ...@@ -122,10 +149,18 @@ class CourseDetails(object):
except ItemNotFoundError: except ItemNotFoundError:
about_item = store.create_xblock(course.runtime, course.id, 'about', about_key) about_item = store.create_xblock(course.runtime, course.id, 'about', about_key)
about_item.data = data about_item.data = data
store.update_item(about_item, user.id, allow_not_found=True) store.update_item(about_item, user_id, allow_not_found=True)
@classmethod @classmethod
def update_from_json(cls, course_key, jsondict, user): def update_about_video(cls, course, video_id, user_id):
"""
Updates the Course's about video to the given video ID.
"""
recomposed_video_tag = CourseDetails.recompose_video_tag(video_id)
cls.update_about_item(course, 'video', recomposed_video_tag, user_id)
@classmethod
def update_from_json(cls, course_key, jsondict, user): # pylint: disable=too-many-statements
""" """
Decode the json into CourseDetails and save any changed attrs to the db Decode the json into CourseDetails and save any changed attrs to the db
""" """
...@@ -134,10 +169,11 @@ class CourseDetails(object): ...@@ -134,10 +169,11 @@ class CourseDetails(object):
dirty = False dirty = False
# In the descriptor's setter, the date is converted to JSON using Date's to_json method. # In the descriptor's setter, the date is converted to JSON
# Calling to_json on something that is already JSON doesn't work. Since reaching directly # using Date's to_json method. Calling to_json on something that
# into the model is nasty, convert the JSON Date to a Python date, which is what the # is already JSON doesn't work. Since reaching directly into the
# setter expects as input. # model is nasty, convert the JSON Date to a Python date, which
# is what the setter expects as input.
date = Date() date = Date()
if 'start_date' in jsondict: if 'start_date' in jsondict:
...@@ -202,25 +238,28 @@ class CourseDetails(object): ...@@ -202,25 +238,28 @@ class CourseDetails(object):
if dirty: if dirty:
module_store.update_item(descriptor, user.id) module_store.update_item(descriptor, user.id)
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed # NOTE: below auto writes to the db w/o verifying that any of
# to make faster, could compare against db or could have client send over a list of which fields changed. # the fields actually changed to make faster, could compare
# against db or could have client send over a list of which
# fields changed.
for attribute in ABOUT_ATTRIBUTES: for attribute in ABOUT_ATTRIBUTES:
if attribute in jsondict: if attribute in jsondict:
cls.update_about_item(course_key, attribute, jsondict[attribute], descriptor, user) cls.update_about_item(descriptor, attribute, jsondict[attribute], user.id)
recomposed_video_tag = CourseDetails.recompose_video_tag(jsondict['intro_video']) cls.update_about_video(descriptor, jsondict['intro_video'], user.id)
cls.update_about_item(course_key, 'video', recomposed_video_tag, descriptor, user)
# Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm # Could just return jsondict w/o doing any db reads, but I put
# it persisted correctly # the reads in as a means to confirm it persisted correctly
return CourseDetails.fetch(course_key) return CourseDetails.fetch(course_key)
@staticmethod @staticmethod
def parse_video_tag(raw_video): def parse_video_tag(raw_video):
""" """
Because the client really only wants the author to specify the youtube key, that's all we send to and get from the client. Because the client really only wants the author to specify the
The problem is that the db stores the html markup as well (which, of course, makes any sitewide changes to how we do videos youtube key, that's all we send to and get from the client. The
next to impossible.) problem is that the db stores the html markup as well (which, of
course, makes any site-wide changes to how we do videos next to
impossible.)
""" """
if not raw_video: if not raw_video:
return None return None
...@@ -237,26 +276,37 @@ class CourseDetails(object): ...@@ -237,26 +276,37 @@ class CourseDetails(object):
@staticmethod @staticmethod
def recompose_video_tag(video_key): def recompose_video_tag(video_key):
# TODO should this use a mako template? Of course, my hope is that this is a short-term workaround for the db not storing """
# the right thing Returns HTML string to embed the video in an iFrame.
"""
# TODO should this use a mako template? Of course, my hope is
# that this is a short-term workaround for the db not storing
# the right thing
result = None result = None
if video_key: if video_key:
result = '<iframe title="YouTube Video" width="560" height="315" src="//www.youtube.com/embed/' + \ result = (
video_key + '?rel=0" frameborder="0" allowfullscreen=""></iframe>' '<iframe title="YouTube Video" width="560" height="315" src="//www.youtube.com/embed/' +
video_key +
'?rel=0" frameborder="0" allowfullscreen=""></iframe>'
)
return result return result
# TODO move to a more general util? # TODO - we may no longer need this - check with Zia
class CourseSettingsEncoder(json.JSONEncoder): def has_active_web_certificate(course):
""" """
Serialize CourseDetails, CourseGradingModel, datetime, and old Locations 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 or course has not enabled `cert_html_view_enabled`
settings.
""" """
def default(self, obj): cert_config = None
if isinstance(obj, (CourseDetails, course_grading.CourseGradingModel)): if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False) and course.cert_html_view_enabled:
return obj.__dict__ cert_config = False
elif isinstance(obj, Location): certificates = getattr(course, 'certificates', {})
return obj.dict() configurations = certificates.get('certificates', [])
elif isinstance(obj, datetime.datetime): for config in configurations:
return Date().to_json(obj) if config.get('is_active'):
else: return True
return JSONEncoder.default(self, obj) return cert_config
"""
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.assertIsNone(details.has_cert_config)
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))
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