Refactor CourseDetails
This commit is contained in:
@@ -2,63 +2,44 @@
|
||||
Tests for Studio Course Settings.
|
||||
"""
|
||||
import datetime
|
||||
import ddt
|
||||
import json
|
||||
import copy
|
||||
import mock
|
||||
from mock import patch
|
||||
import unittest
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.timezone import UTC
|
||||
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 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.tests.factories import UserFactory
|
||||
|
||||
|
||||
from models.settings.course_metadata import CourseMetadata
|
||||
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 util.milestones_helpers import seed_milestone_relationship_types
|
||||
|
||||
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'):
|
||||
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):
|
||||
details = CourseDetails.fetch(self.course.id)
|
||||
jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
|
||||
@@ -87,60 +68,163 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
self.assertEquals(1, jsondetails['number'])
|
||||
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):
|
||||
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
|
||||
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(
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
@override_settings(MKTG_URLS={'ROOT': 'dummy-root'})
|
||||
def test_marketing_site_fetch(self):
|
||||
@@ -296,175 +380,6 @@ class CourseDetailsTestCase(CourseTestCase):
|
||||
self.assertContains(response, "Course Introduction Video")
|
||||
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
|
||||
class CourseGradingTest(CourseTestCase):
|
||||
@@ -1068,7 +983,7 @@ class CourseMetadataEditingTest(CourseTestCase):
|
||||
self.assertEqual(tab_list, course.tabs)
|
||||
|
||||
@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.
|
||||
"""
|
||||
|
||||
@@ -15,6 +15,7 @@ from unittest import skip
|
||||
from django.conf import settings
|
||||
|
||||
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.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import SignalHandler, modulestore
|
||||
@@ -192,26 +193,6 @@ class MixedWithOptionsTestCase(MixedSplitTestCase):
|
||||
with store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
|
||||
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
|
||||
class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
|
||||
@@ -487,7 +468,9 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
|
||||
def _test_course_about_store_index(self, store):
|
||||
""" Test that informational properties in the about store end up in the course_info index """
|
||||
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)
|
||||
response = self.searcher.search(
|
||||
doc_type=CourseAboutSearchIndexer.DISCOVERY_DOCUMENT_TYPE,
|
||||
|
||||
@@ -3,7 +3,6 @@ Common utility functions useful throughout the contentstore
|
||||
"""
|
||||
|
||||
import logging
|
||||
from opaque_keys import InvalidKeyError
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pytz import UTC
|
||||
@@ -14,7 +13,6 @@ from django.utils.translation import ugettext as _
|
||||
from django_comment_common.models import assign_default_role
|
||||
from django_comment_common.utils import seed_permissions_roles
|
||||
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
@@ -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)
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Retrieve user partition information for an XBlock for display in editors.
|
||||
|
||||
@@ -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 edxmako.shortcuts import render_to_response
|
||||
from microsite_configuration import microsite
|
||||
from models.settings.course_details import CourseDetails, CourseSettingsEncoder
|
||||
from models.settings.course_grading import CourseGradingModel
|
||||
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.credit.api import is_credit_course, get_credit_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.utils import get_programs
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
|
||||
28
cms/djangoapps/models/settings/encoder.py
Normal file
28
cms/djangoapps/models/settings/encoder.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
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 @@
|
||||
import json
|
||||
from contentstore import utils
|
||||
from django.utils.translation import ugettext as _
|
||||
from models.settings.encoder import CourseSettingsEncoder
|
||||
from openedx.core.lib.js_utils import escape_json_dumps
|
||||
from models.settings.course_details import CourseSettingsEncoder
|
||||
%>
|
||||
|
||||
<%block name="header_extras">
|
||||
|
||||
@@ -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.
|
||||
# Note: The 'video' attribute is intentionally excluded as it must be
|
||||
# handled separately; its value maps to an alternate key name.
|
||||
# Reference : cms/djangoapps/models/settings/course_details.py
|
||||
# Reference : from openedx.core.djangoapps.models.course_details.py
|
||||
|
||||
|
||||
ABOUT_ATTRIBUTES = {
|
||||
'effort': "Testing effort",
|
||||
|
||||
0
openedx/core/djangoapps/models/__init__.py
Normal file
0
openedx/core/djangoapps/models/__init__.py
Normal file
@@ -1,20 +1,18 @@
|
||||
"""
|
||||
CourseDetails
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
import datetime
|
||||
import json
|
||||
from json.encoder import JSONEncoder
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from opaque_keys.edx.locations import Location
|
||||
from xmodule.fields import Date
|
||||
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.lib.courses import course_image_url
|
||||
from xmodule.fields import Date
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
# This list represents the attribute keys for a course's 'about' info.
|
||||
# Note: The 'video' attribute is intentionally excluded as it must be
|
||||
# handled separately; its value maps to an alternate key name.
|
||||
@@ -30,8 +28,12 @@ ABOUT_ATTRIBUTES = [
|
||||
|
||||
|
||||
class CourseDetails(object):
|
||||
"""
|
||||
An interface for extracting course information from the modulestore.
|
||||
"""
|
||||
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.course_id = course_id
|
||||
self.run = run
|
||||
@@ -44,7 +46,7 @@ class CourseDetails(object):
|
||||
self.short_description = ""
|
||||
self.overview = "" # html to render as the overview
|
||||
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.course_image_name = ""
|
||||
self.course_image_asset_path = "" # URL of the course image
|
||||
@@ -73,7 +75,8 @@ class CourseDetails(object):
|
||||
@classmethod
|
||||
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)
|
||||
course_details = cls(course_key.org, course_key.course, course_key.run)
|
||||
@@ -86,33 +89,57 @@ class CourseDetails(object):
|
||||
course_details.course_image_name = descriptor.course_image
|
||||
course_details.course_image_asset_path = course_image_url(descriptor)
|
||||
course_details.language = descriptor.language
|
||||
course_details.self_paced = descriptor.self_paced
|
||||
|
||||
# Default course license is "All Rights Reserved"
|
||||
course_details.license = getattr(descriptor, "license", "all-rights-reserved")
|
||||
|
||||
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:
|
||||
value = cls._fetch_about_attribute(course_key, attribute)
|
||||
if value is not None:
|
||||
setattr(course_details, attribute, value)
|
||||
|
||||
raw_video = cls._fetch_about_attribute(course_key, 'video')
|
||||
if raw_video:
|
||||
course_details.intro_video = CourseDetails.parse_video_tag(raw_video)
|
||||
|
||||
return course_details
|
||||
|
||||
@classmethod
|
||||
def update_about_item(cls, course_key, about_key, data, course, user):
|
||||
def fetch_youtube_video_id(cls, course_key):
|
||||
"""
|
||||
Update the about item with the new data blob. If data is None, then
|
||||
delete the about item.
|
||||
Returns the course about video ID.
|
||||
"""
|
||||
temploc = course_key.make_usage_key('about', about_key)
|
||||
store = modulestore()
|
||||
raw_video = cls._fetch_about_attribute(course_key, 'video')
|
||||
if raw_video:
|
||||
return cls.parse_video_tag(raw_video)
|
||||
|
||||
@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
|
||||
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 delete the about item.
|
||||
"""
|
||||
temploc = course.id.make_usage_key('about', about_key)
|
||||
store = store or modulestore()
|
||||
if data is None:
|
||||
try:
|
||||
store.delete_item(temploc, user.id)
|
||||
store.delete_item(temploc, user_id)
|
||||
# Ignore an attempt to delete an item that doesn't exist
|
||||
except ValueError:
|
||||
pass
|
||||
@@ -122,10 +149,18 @@ class CourseDetails(object):
|
||||
except ItemNotFoundError:
|
||||
about_item = store.create_xblock(course.runtime, course.id, 'about', about_key)
|
||||
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
|
||||
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
|
||||
"""
|
||||
@@ -134,10 +169,11 @@ class CourseDetails(object):
|
||||
|
||||
dirty = False
|
||||
|
||||
# In the descriptor's setter, the date is converted to JSON using Date's to_json method.
|
||||
# Calling to_json on something that is already JSON doesn't work. Since reaching directly
|
||||
# into the model is nasty, convert the JSON Date to a Python date, which is what the
|
||||
# setter expects as input.
|
||||
# In the descriptor's setter, the date is converted to JSON
|
||||
# using Date's to_json method. Calling to_json on something that
|
||||
# is already JSON doesn't work. Since reaching directly into the
|
||||
# model is nasty, convert the JSON Date to a Python date, which
|
||||
# is what the setter expects as input.
|
||||
date = Date()
|
||||
|
||||
if 'start_date' in jsondict:
|
||||
@@ -202,25 +238,28 @@ class CourseDetails(object):
|
||||
if dirty:
|
||||
module_store.update_item(descriptor, user.id)
|
||||
|
||||
# NOTE: below auto writes to the db w/o verifying that any of the fields actually changed
|
||||
# to make faster, could compare against db or could have client send over a list of which fields changed.
|
||||
# NOTE: below auto writes to the db w/o verifying that any of
|
||||
# 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:
|
||||
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_item(course_key, 'video', recomposed_video_tag, descriptor, user)
|
||||
cls.update_about_video(descriptor, jsondict['intro_video'], user.id)
|
||||
|
||||
# Could just return jsondict w/o doing any db reads, but I put the reads in as a means to confirm
|
||||
# it persisted correctly
|
||||
# Could just return jsondict w/o doing any db reads, but I put
|
||||
# the reads in as a means to confirm it persisted correctly
|
||||
return CourseDetails.fetch(course_key)
|
||||
|
||||
@staticmethod
|
||||
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.
|
||||
The problem is that the db stores the html markup as well (which, of course, makes any sitewide changes to how we do videos
|
||||
next to impossible.)
|
||||
Because the client really only wants the author to specify the
|
||||
youtube key, that's all we send to and get from the client. The
|
||||
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:
|
||||
return None
|
||||
@@ -237,26 +276,37 @@ class CourseDetails(object):
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
if video_key:
|
||||
result = '<iframe title="YouTube Video" width="560" height="315" src="//www.youtube.com/embed/' + \
|
||||
video_key + '?rel=0" frameborder="0" allowfullscreen=""></iframe>'
|
||||
result = (
|
||||
'<iframe title="YouTube Video" width="560" height="315" src="//www.youtube.com/embed/' +
|
||||
video_key +
|
||||
'?rel=0" frameborder="0" allowfullscreen=""></iframe>'
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
# TODO move to a more general util?
|
||||
class CourseSettingsEncoder(json.JSONEncoder):
|
||||
# TODO - we may no longer need this - check with Zia
|
||||
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):
|
||||
if isinstance(obj, (CourseDetails, course_grading.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)
|
||||
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'):
|
||||
return True
|
||||
return cert_config
|
||||
127
openedx/core/djangoapps/models/tests/test_course_details.py
Normal file
127
openedx/core/djangoapps/models/tests/test_course_details.py
Normal file
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
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))
|
||||
Reference in New Issue
Block a user