Entrance Exam authoring and messaging updates
Multi-commit history: - hide drag functionality for entrance exam section. - hide entrance exam subsection elements e.g. delete, drag, name etc. - show unit/verticals expanded in case of entrance exam - modify code in order to allow user to update entrance exam score from UI. - write down unit tests. - write down Jasmine tests. - add bok-choy test - updated bok-choy test - internationalize string - repositioned sequential block creatori - SOL-221 (entrance exam message) - SOL-199 LMS Part (show entrance exam content) and hide the course navigation bar. - redirect the view in case of entrance exam. - update code structure as per suggestions - write down unit tests - fix pep8 - instead of hiding the exam requirement message, now also showing the exam the completion message (success state). - write down unit test to show exam completion message. - Update code as per review suggestions - update doc string - addressed review suggestions - change sequential message text - css adjustments - added new css class for entrance exam score in studio - added Jasmine test for remaning coverage - sequential message should appear under the context of entrance exam subsection. - updated text in CMS and LMS as per suggestions. - added unit text to insure sequential message should not be present in other chapters rather then entrance exam. - skip setter if empty prerequisite course list - exclude logic from xblock_info.js that is specifically related to entrance exam. - added js tests and updated code as per suggestions - added tests - addressed several PR issues - Several small fixes (style, refactoring) - Fixed score update issue - added some more unit tests. - code suggested changes. - addressed PR feedback
This commit is contained in:
@@ -61,7 +61,11 @@ from .component import (
|
||||
ADVANCED_COMPONENT_TYPES,
|
||||
)
|
||||
from contentstore.tasks import rerun_course
|
||||
from contentstore.views.entrance_exam import create_entrance_exam, delete_entrance_exam
|
||||
from contentstore.views.entrance_exam import (
|
||||
create_entrance_exam,
|
||||
update_entrance_exam,
|
||||
delete_entrance_exam
|
||||
)
|
||||
|
||||
from .library import LIBRARIES_ENABLED
|
||||
from .item import create_xblock_info
|
||||
@@ -896,9 +900,10 @@ def settings_handler(request, course_key_string):
|
||||
# if pre-requisite course feature is enabled set pre-requisite course
|
||||
if prerequisite_course_enabled:
|
||||
prerequisite_course_keys = request.json.get('pre_requisite_courses', [])
|
||||
if not all(is_valid_course_key(course_key) for course_key in prerequisite_course_keys):
|
||||
return JsonResponseBadRequest({"error": _("Invalid prerequisite course key")})
|
||||
set_prerequisite_courses(course_key, prerequisite_course_keys)
|
||||
if prerequisite_course_keys:
|
||||
if not all(is_valid_course_key(course_key) for course_key in prerequisite_course_keys):
|
||||
return JsonResponseBadRequest({"error": _("Invalid prerequisite course key")})
|
||||
set_prerequisite_courses(course_key, prerequisite_course_keys)
|
||||
|
||||
# If the entrance exams feature has been enabled, we'll need to check for some
|
||||
# feature-specific settings and handle them accordingly
|
||||
@@ -908,16 +913,24 @@ def settings_handler(request, course_key_string):
|
||||
course_entrance_exam_present = course_module.entrance_exam_enabled
|
||||
entrance_exam_enabled = request.json.get('entrance_exam_enabled', '') == 'true'
|
||||
ee_min_score_pct = request.json.get('entrance_exam_minimum_score_pct', None)
|
||||
|
||||
# If the entrance exam box on the settings screen has been checked,
|
||||
# and the course does not already have an entrance exam attached...
|
||||
if entrance_exam_enabled and not course_entrance_exam_present:
|
||||
# If the entrance exam box on the settings screen has been checked...
|
||||
if entrance_exam_enabled:
|
||||
# Load the default minimum score threshold from settings, then try to override it
|
||||
entrance_exam_minimum_score_pct = float(settings.ENTRANCE_EXAM_MIN_SCORE_PCT)
|
||||
if ee_min_score_pct and ee_min_score_pct != '':
|
||||
if ee_min_score_pct:
|
||||
entrance_exam_minimum_score_pct = float(ee_min_score_pct)
|
||||
# Create the entrance exam
|
||||
create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct)
|
||||
if entrance_exam_minimum_score_pct.is_integer():
|
||||
entrance_exam_minimum_score_pct = entrance_exam_minimum_score_pct / 100
|
||||
entrance_exam_minimum_score_pct = unicode(entrance_exam_minimum_score_pct)
|
||||
# If there's already an entrance exam defined, we'll update the existing one
|
||||
if course_entrance_exam_present:
|
||||
exam_data = {
|
||||
'entrance_exam_minimum_score_pct': entrance_exam_minimum_score_pct
|
||||
}
|
||||
update_entrance_exam(request, course_key, exam_data)
|
||||
# If there's no entrance exam defined, we'll create a new one
|
||||
else:
|
||||
create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct)
|
||||
|
||||
# If the entrance exam box on the settings screen has been unchecked,
|
||||
# and the course has an entrance exam attached...
|
||||
|
||||
@@ -21,12 +21,25 @@ from util.milestones_helpers import generate_milestone_namespace, NAMESPACE_CHOI
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
__all__ = ['entrance_exam', ]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
def _get_default_entrance_exam_minimum_pct():
|
||||
"""
|
||||
Helper method to return the default value from configuration
|
||||
Converts integer values to decimals, since that what we use internally
|
||||
"""
|
||||
entrance_exam_minimum_score_pct = float(settings.ENTRANCE_EXAM_MIN_SCORE_PCT)
|
||||
if entrance_exam_minimum_score_pct.is_integer():
|
||||
entrance_exam_minimum_score_pct = entrance_exam_minimum_score_pct / 100
|
||||
return entrance_exam_minimum_score_pct
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def entrance_exam(request, course_key_string):
|
||||
@@ -60,7 +73,7 @@ def entrance_exam(request, course_key_string):
|
||||
ee_min_score = request.POST.get('entrance_exam_minimum_score_pct', None)
|
||||
|
||||
# if request contains empty value or none then save the default one.
|
||||
entrance_exam_minimum_score_pct = float(settings.ENTRANCE_EXAM_MIN_SCORE_PCT)
|
||||
entrance_exam_minimum_score_pct = _get_default_entrance_exam_minimum_pct()
|
||||
if ee_min_score != '' and ee_min_score is not None:
|
||||
entrance_exam_minimum_score_pct = float(ee_min_score)
|
||||
return create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct)
|
||||
@@ -94,7 +107,7 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N
|
||||
"""
|
||||
# Provide a default value for the minimum score percent if nothing specified
|
||||
if entrance_exam_minimum_score_pct is None:
|
||||
entrance_exam_minimum_score_pct = float(settings.ENTRANCE_EXAM_MIN_SCORE_PCT)
|
||||
entrance_exam_minimum_score_pct = _get_default_entrance_exam_minimum_pct()
|
||||
|
||||
# Confirm the course exists
|
||||
course = modulestore().get_course(course_key)
|
||||
@@ -123,11 +136,19 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N
|
||||
course = modulestore().get_course(course_key)
|
||||
metadata = {
|
||||
'entrance_exam_enabled': True,
|
||||
'entrance_exam_minimum_score_pct': entrance_exam_minimum_score_pct / 100,
|
||||
'entrance_exam_minimum_score_pct': unicode(entrance_exam_minimum_score_pct),
|
||||
'entrance_exam_id': unicode(created_block.location),
|
||||
}
|
||||
CourseMetadata.update_from_dict(metadata, course, request.user)
|
||||
|
||||
# Create the entrance exam section item.
|
||||
create_xblock(
|
||||
parent_locator=unicode(created_block.location),
|
||||
user=request.user,
|
||||
category='sequential',
|
||||
display_name=_('Entrance Exam - Subsection')
|
||||
)
|
||||
|
||||
# Add an entrance exam milestone if one does not already exist
|
||||
milestone_namespace = generate_milestone_namespace(
|
||||
NAMESPACE_CHOICES['ENTRANCE_EXAM'],
|
||||
@@ -181,6 +202,19 @@ def _get_entrance_exam(request, course_key): # pylint: disable=W0613
|
||||
return HttpResponse(status=404)
|
||||
|
||||
|
||||
def update_entrance_exam(request, course_key, exam_data):
|
||||
"""
|
||||
Operation to update course fields pertaining to entrance exams
|
||||
The update operation is not currently exposed directly via the API
|
||||
Because the operation is not exposed directly, we do not return a 200 response
|
||||
But we do return a 400 in the error case because the workflow is executed in a request context
|
||||
"""
|
||||
course = modulestore().get_course(course_key)
|
||||
if course:
|
||||
metadata = exam_data
|
||||
CourseMetadata.update_from_dict(metadata, course, request.user)
|
||||
|
||||
|
||||
def delete_entrance_exam(request, course_key):
|
||||
"""
|
||||
api method to delete an entrance exam
|
||||
|
||||
@@ -781,10 +781,18 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
visibility_state = None
|
||||
published = modulestore().has_published_version(xblock) if not is_library_block else None
|
||||
|
||||
#instead of adding a new feature directly into xblock-info, we should add them into override_type.
|
||||
override_type = {}
|
||||
if getattr(xblock, "is_entrance_exam", None):
|
||||
override_type['is_entrance_exam'] = xblock.is_entrance_exam
|
||||
# defining the default value 'True' for delete, drag and add new child actions in xblock_actions for each xblock.
|
||||
xblock_actions = {'deletable': True, 'draggable': True, 'childAddable': True}
|
||||
explanatory_message = None
|
||||
# is_entrance_exam is inherited metadata.
|
||||
if xblock.category == 'chapter' and getattr(xblock, "is_entrance_exam", None):
|
||||
# Entrance exam section should not be deletable, draggable and not have 'New Subsection' button.
|
||||
xblock_actions['deletable'] = xblock_actions['childAddable'] = xblock_actions['draggable'] = False
|
||||
if parent_xblock is None:
|
||||
parent_xblock = get_parent_xblock(xblock)
|
||||
|
||||
explanatory_message = _('Students must score {score}% or higher to access course materials.').format(
|
||||
score=int(parent_xblock.entrance_exam_minimum_score_pct * 100))
|
||||
|
||||
xblock_info = {
|
||||
"id": unicode(xblock.location),
|
||||
@@ -805,8 +813,14 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
|
||||
"format": xblock.format,
|
||||
"course_graders": json.dumps([grader.get('type') for grader in graders]),
|
||||
"has_changes": has_changes,
|
||||
"override_type": override_type,
|
||||
"actions": xblock_actions,
|
||||
"explanatory_message": explanatory_message
|
||||
}
|
||||
|
||||
# Entrance exam subsection should be hidden. in_entrance_exam is inherited metadata, all children will have it.
|
||||
if xblock.category == 'sequential' and getattr(xblock, "in_entrance_exam", False):
|
||||
xblock_info["is_header_visible"] = False
|
||||
|
||||
if data is not None:
|
||||
xblock_info["data"] = data
|
||||
if metadata is not None:
|
||||
|
||||
@@ -1405,7 +1405,7 @@ class TestXBlockInfo(ItemTest):
|
||||
json_response = json.loads(resp.content)
|
||||
self.validate_course_xblock_info(json_response, course_outline=True)
|
||||
|
||||
def test_chapter_entrance_exam_xblock_info(self):
|
||||
def test_entrance_exam_chapter_xblock_info(self):
|
||||
chapter = ItemFactory.create(
|
||||
parent_location=self.course.location, category='chapter', display_name="Entrance Exam",
|
||||
user_id=self.user.id, is_entrance_exam=True
|
||||
@@ -1416,8 +1416,68 @@ class TestXBlockInfo(ItemTest):
|
||||
include_child_info=True,
|
||||
include_children_predicate=ALWAYS,
|
||||
)
|
||||
self.assertEqual(xblock_info['override_type'], {'is_entrance_exam': True})
|
||||
# entrance exam chapter should not be deletable, draggable and childAddable.
|
||||
actions = xblock_info['actions']
|
||||
self.assertEqual(actions['deletable'], False)
|
||||
self.assertEqual(actions['draggable'], False)
|
||||
self.assertEqual(actions['childAddable'], False)
|
||||
self.assertEqual(xblock_info['display_name'], 'Entrance Exam')
|
||||
self.assertIsNone(xblock_info.get('is_header_visible', None))
|
||||
|
||||
def test_none_entrance_exam_chapter_xblock_info(self):
|
||||
chapter = ItemFactory.create(
|
||||
parent_location=self.course.location, category='chapter', display_name="Test Chapter",
|
||||
user_id=self.user.id
|
||||
)
|
||||
chapter = modulestore().get_item(chapter.location)
|
||||
xblock_info = create_xblock_info(
|
||||
chapter,
|
||||
include_child_info=True,
|
||||
include_children_predicate=ALWAYS,
|
||||
)
|
||||
|
||||
# chapter should be deletable, draggable and childAddable if not an entrance exam.
|
||||
actions = xblock_info['actions']
|
||||
self.assertEqual(actions['deletable'], True)
|
||||
self.assertEqual(actions['draggable'], True)
|
||||
self.assertEqual(actions['childAddable'], True)
|
||||
# chapter xblock info should not contains the key of 'is_header_visible'.
|
||||
self.assertIsNone(xblock_info.get('is_header_visible', None))
|
||||
|
||||
def test_entrance_exam_sequential_xblock_info(self):
|
||||
chapter = ItemFactory.create(
|
||||
parent_location=self.course.location, category='chapter', display_name="Entrance Exam",
|
||||
user_id=self.user.id, is_entrance_exam=True, in_entrance_exam=True
|
||||
)
|
||||
|
||||
subsection = ItemFactory.create(
|
||||
parent_location=chapter.location, category='sequential', display_name="Subsection - Entrance Exam",
|
||||
user_id=self.user.id, in_entrance_exam=True
|
||||
)
|
||||
subsection = modulestore().get_item(subsection.location)
|
||||
xblock_info = create_xblock_info(
|
||||
subsection,
|
||||
include_child_info=True,
|
||||
include_children_predicate=ALWAYS
|
||||
)
|
||||
# in case of entrance exam subsection, header should be hidden.
|
||||
self.assertEqual(xblock_info['is_header_visible'], False)
|
||||
self.assertEqual(xblock_info['display_name'], 'Subsection - Entrance Exam')
|
||||
|
||||
def test_none_entrance_exam_sequential_xblock_info(self):
|
||||
subsection = ItemFactory.create(
|
||||
parent_location=self.chapter.location, category='sequential', display_name="Subsection - Exam",
|
||||
user_id=self.user.id
|
||||
)
|
||||
subsection = modulestore().get_item(subsection.location)
|
||||
xblock_info = create_xblock_info(
|
||||
subsection,
|
||||
include_child_info=True,
|
||||
include_children_predicate=ALWAYS,
|
||||
parent_xblock=self.chapter
|
||||
)
|
||||
# sequential xblock info should not contains the key of 'is_header_visible'.
|
||||
self.assertIsNone(xblock_info.get('is_header_visible', None))
|
||||
|
||||
def test_chapter_xblock_info(self):
|
||||
chapter = modulestore().get_item(self.chapter.location)
|
||||
|
||||
Reference in New Issue
Block a user