Commit b900221c by Matt Drayer

Merge pull request #10394 from edx/asadiqbal08/SOL-1292

asadiqbal08/SOL-1292 Entrance Exam is not imported with an imported course
parents 67f649a2 bf0affc2
...@@ -164,35 +164,7 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N ...@@ -164,35 +164,7 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N
category='sequential', category='sequential',
display_name=_('Entrance Exam - Subsection') display_name=_('Entrance Exam - Subsection')
) )
add_entrance_exam_milestone(course.id, created_block)
# 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
)
return HttpResponse(status=201) return HttpResponse(status=201)
...@@ -250,14 +222,7 @@ def _delete_entrance_exam(request, course_key): ...@@ -250,14 +222,7 @@ def _delete_entrance_exam(request, course_key):
if course is None: if course is None:
return HttpResponse(status=400) return HttpResponse(status=400)
course_children = store.get_items( remove_entrance_exam_milestone_reference(request, course_key)
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))
# Reset the entrance exam flags on the course # Reset the entrance exam flags on the course
# Reload the course so we have the latest state # Reload the course so we have the latest state
...@@ -283,3 +248,50 @@ def _serialize_entrance_exam(entrance_exam_module): ...@@ -283,3 +248,50 @@ def _serialize_entrance_exam(entrance_exam_module):
return json.dumps({ return json.dumps({
'locator': unicode(entrance_exam_module.location) '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))
...@@ -37,6 +37,11 @@ from student.auth import has_course_author_access ...@@ -37,6 +37,11 @@ from student.auth import has_course_author_access
from openedx.core.lib.extract_tar import safetar_extractall from openedx.core.lib.extract_tar import safetar_extractall
from util.json_request import JsonResponse from util.json_request import JsonResponse
from util.views import ensure_valid_course_key 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 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_ ...@@ -110,6 +115,17 @@ def _import_handler(request, courselike_key, root_name, successful_url, context_
session_status = request.session.setdefault("import_status", {}) session_status = request.session.setdefault("import_status", {})
courselike_string = unicode(courselike_key) + filename courselike_string = unicode(courselike_key) + filename
_save_request_status(request, courselike_string, 0) _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'): if not filename.endswith('.tar.gz'):
_save_request_status(request, courselike_string, -1) _save_request_status(request, courselike_string, -1)
return JsonResponse( return JsonResponse(
...@@ -300,6 +316,22 @@ def _import_handler(request, courselike_key, root_name, successful_url, context_ ...@@ -300,6 +316,22 @@ def _import_handler(request, courselike_key, root_name, successful_url, context_
if session_status[courselike_string] != 4: if session_status[courselike_string] != 4:
_save_request_status(request, courselike_string, -abs(session_status[courselike_string])) _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'}) return JsonResponse({'Status': 'OK'})
elif request.method == 'GET': # assume html elif request.method == 'GET': # assume html
status_url = reverse_course_url( status_url = reverse_course_url(
......
...@@ -10,7 +10,8 @@ from django.test.client import RequestFactory ...@@ -10,7 +10,8 @@ from django.test.client import RequestFactory
from contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase from contentstore.tests.utils import AjaxEnabledTestClient, CourseTestCase
from contentstore.utils import reverse_url 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 contentstore.views.helpers import GRADER_TYPES
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
...@@ -18,6 +19,7 @@ from opaque_keys.edx.keys import UsageKey ...@@ -18,6 +19,7 @@ from opaque_keys.edx.keys import UsageKey
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from util import milestones_helpers from util import milestones_helpers
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from contentstore.views.helpers import create_xblock
@patch.dict(settings.FEATURES, {'ENTRANCE_EXAMS': True}) @patch.dict(settings.FEATURES, {'ENTRANCE_EXAMS': True})
...@@ -37,6 +39,57 @@ class EntranceExamHandlerTests(CourseTestCase): ...@@ -37,6 +39,57 @@ class EntranceExamHandlerTests(CourseTestCase):
milestones_helpers.seed_milestone_relationship_types() milestones_helpers.seed_milestone_relationship_types()
self.milestone_relationship_types = milestones_helpers.get_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): def test_contentstore_views_entrance_exam_post(self):
""" """
Unit Test: test_contentstore_views_entrance_exam_post Unit Test: test_contentstore_views_entrance_exam_post
......
...@@ -26,6 +26,10 @@ from contentstore.tests.utils import CourseTestCase ...@@ -26,6 +26,10 @@ from contentstore.tests.utils import CourseTestCase
from openedx.core.lib.extract_tar import safetar_extractall from openedx.core.lib.extract_tar import safetar_extractall
from student import auth from student import auth
from student.roles import CourseInstructorRole, CourseStaffRole 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 = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
...@@ -35,6 +39,92 @@ log = logging.getLogger(__name__) ...@@ -35,6 +39,92 @@ log = logging.getLogger(__name__)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) @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('<course url_name="2013_Spring" org="EDx" course="0.00x"/>')
with open(os.path.join(embedded_exam_dir, "course", "2013_Spring.xml"), "w+") as f:
f.write(
'<course '
'entrance_exam_enabled="true" entrance_exam_id="xyz" entrance_exam_minimum_score_pct="0.7">'
'<chapter url_name="2015_chapter_entrance_exam"/></course>'
)
with open(os.path.join(embedded_exam_dir, "chapter", "2015_chapter_entrance_exam.xml"), "w+") as f:
f.write('<chapter display_name="Entrance Exam" in_entrance_exam="true" is_entrance_exam="true"></chapter>')
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): class ImportTestCase(CourseTestCase):
""" """
Unit tests for importing a course or Library Unit tests for importing a course or Library
......
...@@ -1459,7 +1459,7 @@ class TestXBlockInfo(ItemTest): ...@@ -1459,7 +1459,7 @@ class TestXBlockInfo(ItemTest):
self.validate_course_xblock_info(json_response, course_outline=True) self.validate_course_xblock_info(json_response, course_outline=True)
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.split, 5, 5), (ModuleStoreEnum.Type.split, 4, 4),
(ModuleStoreEnum.Type.mongo, 5, 7), (ModuleStoreEnum.Type.mongo, 5, 7),
) )
@ddt.unpack @ddt.unpack
......
...@@ -108,9 +108,9 @@ class CountMongoCallsCourseTraversal(TestCase): ...@@ -108,9 +108,9 @@ class CountMongoCallsCourseTraversal(TestCase):
# The line below shows the way this traversal *should* be done # 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). # (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, 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, 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, False, False, 4),
(MIXED_SPLIT_MODULESTORE_BUILDER, None, True, False, 4), (MIXED_SPLIT_MODULESTORE_BUILDER, None, True, False, 4),
# TODO: The call count below seems like a bug - should be 4? # TODO: The call count below seems like a bug - should be 4?
......
...@@ -52,7 +52,7 @@ class SequenceFields(object): ...@@ -52,7 +52,7 @@ class SequenceFields(object):
"Note, you must enable Entrance Exams for this course setting to take effect." "Note, you must enable Entrance Exams for this course setting to take effect."
), ),
default=False, default=False,
scope=Scope.content, scope=Scope.settings,
) )
......
...@@ -12,6 +12,8 @@ from ...pages.studio.import_export import ExportLibraryPage, ExportCoursePage, I ...@@ -12,6 +12,8 @@ from ...pages.studio.import_export import ExportLibraryPage, ExportCoursePage, I
from ...pages.studio.library import LibraryEditPage from ...pages.studio.library import LibraryEditPage
from ...pages.studio.container import ContainerPage from ...pages.studio.container import ContainerPage
from ...pages.studio.overview import CourseOutlinePage from ...pages.studio.overview import CourseOutlinePage
from ...pages.lms.courseware import CoursewarePage
from ...pages.lms.staff_view import StaffPage
class ExportTestMixin(object): class ExportTestMixin(object):
...@@ -269,6 +271,51 @@ class ImportTestMixin(object): ...@@ -269,6 +271,51 @@ class ImportTestMixin(object):
self.import_page.wait_for_tasks(fail_on='Updating') 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): class TestCourseImport(ImportTestMixin, StudioCourseTest):
""" """
Tests the Course import page Tests the Course import page
......
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