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:
asadiqbal
2015-01-29 18:18:38 +05:00
committed by Matt Drayer
parent 57c38649ba
commit 5a7ac441e5
19 changed files with 669 additions and 102 deletions

View File

@@ -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...

View File

@@ -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

View File

@@ -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:

View File

@@ -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)