diff --git a/cms/djangoapps/contentstore/views/entrance_exam.py b/cms/djangoapps/contentstore/views/entrance_exam.py
index 320ce3df3a..8087b5a02a 100644
--- a/cms/djangoapps/contentstore/views/entrance_exam.py
+++ b/cms/djangoapps/contentstore/views/entrance_exam.py
@@ -164,35 +164,7 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N
category='sequential',
display_name=_('Entrance Exam - Subsection')
)
-
- # Add an entrance exam milestone if one does not already exist
- namespace_choices = milestones_helpers.get_namespace_choices()
- milestone_namespace = milestones_helpers.generate_milestone_namespace(
- namespace_choices.get('ENTRANCE_EXAM'),
- course_key
- )
- milestones = milestones_helpers.get_milestones(milestone_namespace)
- if len(milestones):
- milestone = milestones[0]
- else:
- description = 'Autogenerated during {} entrance exam creation.'.format(unicode(course.id))
- milestone = milestones_helpers.add_milestone({
- 'name': _('Completed Course Entrance Exam'),
- 'namespace': milestone_namespace,
- 'description': description
- })
- relationship_types = milestones_helpers.get_milestone_relationship_types()
- milestones_helpers.add_course_milestone(
- unicode(course.id),
- relationship_types['REQUIRES'],
- milestone
- )
- milestones_helpers.add_course_content_milestone(
- unicode(course.id),
- unicode(created_block.location),
- relationship_types['FULFILLS'],
- milestone
- )
+ add_entrance_exam_milestone(course.id, created_block)
return HttpResponse(status=201)
@@ -250,14 +222,7 @@ def _delete_entrance_exam(request, course_key):
if course is None:
return HttpResponse(status=400)
- course_children = store.get_items(
- course_key,
- qualifiers={'category': 'chapter'}
- )
- for course_child in course_children:
- if course_child.is_entrance_exam:
- delete_item(request, course_child.scope_ids.usage_id)
- milestones_helpers.remove_content_references(unicode(course_child.scope_ids.usage_id))
+ remove_entrance_exam_milestone_reference(request, course_key)
# Reset the entrance exam flags on the course
# Reload the course so we have the latest state
@@ -283,3 +248,50 @@ def _serialize_entrance_exam(entrance_exam_module):
return json.dumps({
'locator': unicode(entrance_exam_module.location)
})
+
+
+def add_entrance_exam_milestone(course_id, x_block):
+ # Add an entrance exam milestone if one does not already exist for given xBlock
+ # As this is a standalone method for entrance exam, We should check that given xBlock should be an entrance exam.
+ if x_block.is_entrance_exam:
+ namespace_choices = milestones_helpers.get_namespace_choices()
+ milestone_namespace = milestones_helpers.generate_milestone_namespace(
+ namespace_choices.get('ENTRANCE_EXAM'),
+ course_id
+ )
+ milestones = milestones_helpers.get_milestones(milestone_namespace)
+ if len(milestones):
+ milestone = milestones[0]
+ else:
+ description = 'Autogenerated during {} entrance exam creation.'.format(unicode(course_id))
+ milestone = milestones_helpers.add_milestone({
+ 'name': _('Completed Course Entrance Exam'),
+ 'namespace': milestone_namespace,
+ 'description': description
+ })
+ relationship_types = milestones_helpers.get_milestone_relationship_types()
+ milestones_helpers.add_course_milestone(
+ unicode(course_id),
+ relationship_types['REQUIRES'],
+ milestone
+ )
+ milestones_helpers.add_course_content_milestone(
+ unicode(course_id),
+ unicode(x_block.location),
+ relationship_types['FULFILLS'],
+ milestone
+ )
+
+
+def remove_entrance_exam_milestone_reference(request, course_key):
+ """
+ Remove content reference for entrance exam.
+ """
+ course_children = modulestore().get_items(
+ course_key,
+ qualifiers={'category': 'chapter'}
+ )
+ for course_child in course_children:
+ if course_child.is_entrance_exam:
+ delete_item(request, course_child.scope_ids.usage_id)
+ milestones_helpers.remove_content_references(unicode(course_child.scope_ids.usage_id))
diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py
index fbb786d680..58187d3b53 100644
--- a/cms/djangoapps/contentstore/views/import_export.py
+++ b/cms/djangoapps/contentstore/views/import_export.py
@@ -37,6 +37,11 @@ from student.auth import has_course_author_access
from openedx.core.lib.extract_tar import safetar_extractall
from util.json_request import JsonResponse
from util.views import ensure_valid_course_key
+from models.settings.course_metadata import CourseMetadata
+from contentstore.views.entrance_exam import (
+ add_entrance_exam_milestone,
+ remove_entrance_exam_milestone_reference
+)
from contentstore.utils import reverse_course_url, reverse_usage_url, reverse_library_url
@@ -110,6 +115,17 @@ def _import_handler(request, courselike_key, root_name, successful_url, context_
session_status = request.session.setdefault("import_status", {})
courselike_string = unicode(courselike_key) + filename
_save_request_status(request, courselike_string, 0)
+
+ # If the course has an entrance exam then remove it and its corresponding milestone.
+ # current course state before import.
+ if root_name == COURSE_ROOT:
+ if courselike_module.entrance_exam_enabled:
+ remove_entrance_exam_milestone_reference(request, courselike_key)
+ log.info(
+ "entrance exam milestone content reference for course %s has been removed",
+ courselike_module.id
+ )
+
if not filename.endswith('.tar.gz'):
_save_request_status(request, courselike_string, -1)
return JsonResponse(
@@ -300,6 +316,22 @@ def _import_handler(request, courselike_key, root_name, successful_url, context_
if session_status[courselike_string] != 4:
_save_request_status(request, courselike_string, -abs(session_status[courselike_string]))
+ # status == 4 represents that course has been imported successfully.
+ if session_status[courselike_string] == 4 and root_name == COURSE_ROOT:
+ # Reload the course so we have the latest state
+ course = modulestore().get_course(courselike_key)
+ if course.entrance_exam_enabled:
+ entrance_exam_chapter = modulestore().get_items(
+ course.id,
+ qualifiers={'category': 'chapter'},
+ settings={'is_entrance_exam': True}
+ )[0]
+
+ metadata = {'entrance_exam_id': unicode(entrance_exam_chapter.location)}
+ CourseMetadata.update_from_dict(metadata, course, request.user)
+ add_entrance_exam_milestone(course.id, entrance_exam_chapter)
+ log.info("Course %s Entrance exam imported", course.id)
+
return JsonResponse({'Status': 'OK'})
elif request.method == 'GET': # assume html
status_url = reverse_course_url(
diff --git a/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py b/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py
index dfcd1de091..a773f5e0e2 100644
--- a/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py
+++ b/cms/djangoapps/contentstore/views/tests/test_entrance_exam.py
@@ -10,7 +10,8 @@ from django.test.client import RequestFactory
from contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase
from contentstore.utils import reverse_url
-from contentstore.views.entrance_exam import create_entrance_exam, update_entrance_exam, delete_entrance_exam
+from contentstore.views.entrance_exam import create_entrance_exam, update_entrance_exam, delete_entrance_exam,\
+ add_entrance_exam_milestone, remove_entrance_exam_milestone_reference
from contentstore.views.helpers import GRADER_TYPES
from models.settings.course_grading import CourseGradingModel
from models.settings.course_metadata import CourseMetadata
@@ -18,6 +19,7 @@ from opaque_keys.edx.keys import UsageKey
from student.tests.factories import UserFactory
from util import milestones_helpers
from xmodule.modulestore.django import modulestore
+from contentstore.views.helpers import create_xblock
@patch.dict(settings.FEATURES, {'ENTRANCE_EXAMS': True})
@@ -37,6 +39,57 @@ class EntranceExamHandlerTests(CourseTestCase):
milestones_helpers.seed_milestone_relationship_types()
self.milestone_relationship_types = milestones_helpers.get_milestone_relationship_types()
+ def test_entrance_exam_milestone_addition(self):
+ """
+ Unit Test: test addition of entrance exam milestone content
+ """
+ parent_locator = unicode(self.course.location)
+ created_block = create_xblock(
+ parent_locator=parent_locator,
+ user=self.user,
+ category='chapter',
+ display_name=('Entrance Exam'),
+ is_entrance_exam=True
+ )
+ add_entrance_exam_milestone(self.course.id, created_block)
+ content_milestones = milestones_helpers.get_course_content_milestones(
+ unicode(self.course.id),
+ unicode(created_block.location),
+ self.milestone_relationship_types['FULFILLS']
+ )
+ self.assertTrue(len(content_milestones))
+ self.assertEqual(len(milestones_helpers.get_course_milestones(self.course.id)), 1)
+
+ def test_entrance_exam_milestone_removal(self):
+ """
+ Unit Test: test removal of entrance exam milestone content
+ """
+ parent_locator = unicode(self.course.location)
+ created_block = create_xblock(
+ parent_locator=parent_locator,
+ user=self.user,
+ category='chapter',
+ display_name=('Entrance Exam'),
+ is_entrance_exam=True
+ )
+ add_entrance_exam_milestone(self.course.id, created_block)
+ content_milestones = milestones_helpers.get_course_content_milestones(
+ unicode(self.course.id),
+ unicode(created_block.location),
+ self.milestone_relationship_types['FULFILLS']
+ )
+ self.assertEqual(len(content_milestones), 1)
+ user = UserFactory()
+ request = RequestFactory().request()
+ request.user = user
+ remove_entrance_exam_milestone_reference(request, self.course.id)
+ content_milestones = milestones_helpers.get_course_content_milestones(
+ unicode(self.course.id),
+ unicode(created_block.location),
+ self.milestone_relationship_types['FULFILLS']
+ )
+ self.assertEqual(len(content_milestones), 0)
+
def test_contentstore_views_entrance_exam_post(self):
"""
Unit Test: test_contentstore_views_entrance_exam_post
diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py
index 60548896c7..b26c93407e 100644
--- a/cms/djangoapps/contentstore/views/tests/test_import_export.py
+++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py
@@ -26,6 +26,10 @@ from contentstore.tests.utils import CourseTestCase
from openedx.core.lib.extract_tar import safetar_extractall
from student import auth
from student.roles import CourseInstructorRole, CourseStaffRole
+from util.milestones_helpers import seed_milestone_relationship_types
+from models.settings.course_metadata import CourseMetadata
+from util import milestones_helpers
+from xmodule.modulestore.django import modulestore
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
@@ -34,6 +38,92 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
log = logging.getLogger(__name__)
+@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
+class ImportEntranceExamTestCase(CourseTestCase):
+ """
+ Unit tests for importing a course with entrance exam
+ """
+ def setUp(self):
+ super(ImportEntranceExamTestCase, self).setUp()
+ self.url = reverse_course_url('import_handler', self.course.id)
+ self.content_dir = path(tempfile.mkdtemp())
+ self.addCleanup(shutil.rmtree, self.content_dir)
+
+ # Create tar test file -----------------------------------------------
+ # OK course with entrance exam section:
+ seed_milestone_relationship_types()
+ entrance_exam_dir = tempfile.mkdtemp(dir=self.content_dir)
+ # test course being deeper down than top of tar file
+ embedded_exam_dir = os.path.join(entrance_exam_dir, "grandparent", "parent")
+ os.makedirs(os.path.join(embedded_exam_dir, "course"))
+ os.makedirs(os.path.join(embedded_exam_dir, "chapter"))
+ with open(os.path.join(embedded_exam_dir, "course.xml"), "w+") as f:
+ f.write('')
+
+ with open(os.path.join(embedded_exam_dir, "course", "2013_Spring.xml"), "w+") as f:
+ f.write(
+ ''
+ ''
+ )
+
+ with open(os.path.join(embedded_exam_dir, "chapter", "2015_chapter_entrance_exam.xml"), "w+") as f:
+ f.write('')
+
+ self.entrance_exam_tar = os.path.join(self.content_dir, "entrance_exam.tar.gz")
+ with tarfile.open(self.entrance_exam_tar, "w:gz") as gtar:
+ gtar.add(entrance_exam_dir)
+
+ def test_import_existing_entrance_exam_course(self):
+ """
+ Check that course is imported successfully as an entrance exam.
+ """
+ course = self.store.get_course(self.course.id)
+ self.assertIsNotNone(course)
+ self.assertEquals(course.entrance_exam_enabled, False)
+
+ with open(self.entrance_exam_tar) as gtar:
+ args = {"name": self.entrance_exam_tar, "course-data": [gtar]}
+ resp = self.client.post(self.url, args)
+ self.assertEquals(resp.status_code, 200)
+ course = self.store.get_course(self.course.id)
+ self.assertIsNotNone(course)
+ self.assertEquals(course.entrance_exam_enabled, True)
+ self.assertEquals(course.entrance_exam_minimum_score_pct, 0.7)
+
+ def test_import_delete_pre_exiting_entrance_exam(self):
+ """
+ Check that pre existed entrance exam content should be overwrite with the imported course.
+ """
+ exam_url = '/course/{}/entrance_exam/'.format(unicode(self.course.id))
+ resp = self.client.post(exam_url, {'entrance_exam_minimum_score_pct': 0.5}, http_accept='application/json')
+ self.assertEqual(resp.status_code, 201)
+
+ # Reload the test course now that the exam module has been added
+ self.course = modulestore().get_course(self.course.id)
+ metadata = CourseMetadata.fetch_all(self.course)
+ self.assertTrue(metadata['entrance_exam_enabled'])
+ self.assertIsNotNone(metadata['entrance_exam_minimum_score_pct'])
+ self.assertEqual(metadata['entrance_exam_minimum_score_pct']['value'], 0.5)
+ self.assertTrue(len(milestones_helpers.get_course_milestones(unicode(self.course.id))))
+ content_milestones = milestones_helpers.get_course_content_milestones(
+ unicode(self.course.id),
+ metadata['entrance_exam_id']['value'],
+ milestones_helpers.get_milestone_relationship_types()['FULFILLS']
+ )
+ self.assertTrue(len(content_milestones))
+
+ # Now import entrance exam course
+ with open(self.entrance_exam_tar) as gtar:
+ args = {"name": self.entrance_exam_tar, "course-data": [gtar]}
+ resp = self.client.post(self.url, args)
+ self.assertEquals(resp.status_code, 200)
+ course = self.store.get_course(self.course.id)
+ self.assertIsNotNone(course)
+ self.assertEquals(course.entrance_exam_enabled, True)
+ self.assertEquals(course.entrance_exam_minimum_score_pct, 0.7)
+
+
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class ImportTestCase(CourseTestCase):
"""
diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py
index f633e0b170..7784184a44 100644
--- a/cms/djangoapps/contentstore/views/tests/test_item.py
+++ b/cms/djangoapps/contentstore/views/tests/test_item.py
@@ -1459,7 +1459,7 @@ class TestXBlockInfo(ItemTest):
self.validate_course_xblock_info(json_response, course_outline=True)
@ddt.data(
- (ModuleStoreEnum.Type.split, 5, 5),
+ (ModuleStoreEnum.Type.split, 4, 4),
(ModuleStoreEnum.Type.mongo, 5, 7),
)
@ddt.unpack
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py
index 3cbdd2c39f..4e9b336bc8 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo_call_count.py
@@ -108,9 +108,9 @@ class CountMongoCallsCourseTraversal(TestCase):
# The line below shows the way this traversal *should* be done
# (if you'll eventually access all the fields and load all the definitions anyway).
(MIXED_SPLIT_MODULESTORE_BUILDER, None, False, True, 4),
- (MIXED_SPLIT_MODULESTORE_BUILDER, None, True, True, 143),
+ (MIXED_SPLIT_MODULESTORE_BUILDER, None, True, True, 41),
(MIXED_SPLIT_MODULESTORE_BUILDER, 0, False, True, 143),
- (MIXED_SPLIT_MODULESTORE_BUILDER, 0, True, True, 143),
+ (MIXED_SPLIT_MODULESTORE_BUILDER, 0, True, True, 41),
(MIXED_SPLIT_MODULESTORE_BUILDER, None, False, False, 4),
(MIXED_SPLIT_MODULESTORE_BUILDER, None, True, False, 4),
# TODO: The call count below seems like a bug - should be 4?
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index f9b3d1cdbf..b7143e762d 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -52,7 +52,7 @@ class SequenceFields(object):
"Note, you must enable Entrance Exams for this course setting to take effect."
),
default=False,
- scope=Scope.content,
+ scope=Scope.settings,
)
diff --git a/common/test/acceptance/tests/studio/test_import_export.py b/common/test/acceptance/tests/studio/test_import_export.py
index 7fa9fa445a..ecfb0eaeac 100644
--- a/common/test/acceptance/tests/studio/test_import_export.py
+++ b/common/test/acceptance/tests/studio/test_import_export.py
@@ -12,6 +12,8 @@ from ...pages.studio.import_export import ExportLibraryPage, ExportCoursePage, I
from ...pages.studio.library import LibraryEditPage
from ...pages.studio.container import ContainerPage
from ...pages.studio.overview import CourseOutlinePage
+from ...pages.lms.courseware import CoursewarePage
+from ...pages.lms.staff_view import StaffPage
class ExportTestMixin(object):
@@ -269,6 +271,51 @@ class ImportTestMixin(object):
self.import_page.wait_for_tasks(fail_on='Updating')
+class TestEntranceExamCourseImport(ImportTestMixin, StudioCourseTest):
+ """
+ Tests the Course import page
+ """
+ tarball_name = 'entrance_exam_course.2015.tar.gz'
+ bad_tarball_name = 'bad_course.tar.gz'
+ import_page_class = ImportCoursePage
+ landing_page_class = CourseOutlinePage
+
+ def page_args(self):
+ return [self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']]
+
+ def test_course_updated_with_entrance_exam(self):
+ """
+ Given that I visit an empty course before import
+ I should not see a section named 'Section' or 'Entrance Exam'
+ When I visit the import page
+ And I upload a course that has an entrance exam section named 'Entrance Exam'
+ And I visit the course outline page again
+ The section named 'Entrance Exam' should now be available.
+ And when I switch the view mode to student view and Visit CourseWare
+ Then I see one section in the sidebar that is 'Entrance Exam'
+ """
+ self.landing_page.visit()
+ # Should not exist yet.
+ self.assertRaises(IndexError, self.landing_page.section, "Section")
+ self.assertRaises(IndexError, self.landing_page.section, "Entrance Exam")
+ self.import_page.visit()
+ self.import_page.upload_tarball(self.tarball_name)
+ self.import_page.wait_for_upload()
+ self.landing_page.visit()
+ # There should be two sections. 'Entrance Exam' and 'Section' on the landing page.
+ self.landing_page.section("Entrance Exam")
+ self.landing_page.section("Section")
+
+ self.landing_page.view_live()
+ courseware = CoursewarePage(self.browser, self.course_id)
+ courseware.wait_for_page()
+ StaffPage(self.browser, self.course_id).set_staff_view_mode('Student')
+ self.assertEqual(courseware.num_sections, 1)
+ self.assertIn(
+ "To access course materials, you must score", courseware.entrance_exam_message_selector.text[0]
+ )
+
+
class TestCourseImport(ImportTestMixin, StudioCourseTest):
"""
Tests the Course import page
diff --git a/common/test/data/imports/entrance_exam_course.2015.tar.gz b/common/test/data/imports/entrance_exam_course.2015.tar.gz
new file mode 100644
index 0000000000..7dbb610746
Binary files /dev/null and b/common/test/data/imports/entrance_exam_course.2015.tar.gz differ