diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py
index c041f72f5b..8af9fbba65 100644
--- a/cms/djangoapps/contentstore/tests/test_course_settings.py
+++ b/cms/djangoapps/contentstore/tests/test_course_settings.py
@@ -150,7 +150,7 @@ class CourseDetailsTestCase(CourseTestCase):
MilestoneRelationshipType.objects.create(name='fulfills')
@patch.dict(settings.FEATURES, {'ENTRANCE_EXAMS': True})
- def test_entrance_exam_created_and_deleted_successfully(self):
+ def test_entrance_exam_created_updated_and_deleted_successfully(self):
self._seed_milestone_relationship_types()
settings_details_url = get_url(self.course.id)
data = {
@@ -169,6 +169,20 @@ class CourseDetailsTestCase(CourseTestCase):
self.assertTrue(course.entrance_exam_enabled)
self.assertEquals(course.entrance_exam_minimum_score_pct, .60)
+ # Update the entrance exam
+ data['entrance_exam_enabled'] = "true"
+ data['entrance_exam_minimum_score_pct'] = "80"
+ response = self.client.post(
+ settings_details_url,
+ data=json.dumps(data),
+ content_type='application/json',
+ HTTP_ACCEPT='application/json'
+ )
+ self.assertEquals(response.status_code, 200)
+ course = modulestore().get_course(self.course.id)
+ self.assertTrue(course.entrance_exam_enabled)
+ self.assertEquals(course.entrance_exam_minimum_score_pct, .80)
+
# Delete the entrance exam
data['entrance_exam_enabled'] = "false"
response = self.client.post(
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index 6d01c3424c..7db80dc44f 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -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...
diff --git a/cms/djangoapps/contentstore/views/entrance_exam.py b/cms/djangoapps/contentstore/views/entrance_exam.py
index 7c8658a4fe..08da658e76 100644
--- a/cms/djangoapps/contentstore/views/entrance_exam.py
+++ b/cms/djangoapps/contentstore/views/entrance_exam.py
@@ -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
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index 86cb005f17..507e35962a 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -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:
diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py
index bb55cb39aa..908fd784d2 100644
--- a/cms/djangoapps/contentstore/views/tests/test_item.py
+++ b/cms/djangoapps/contentstore/views/tests/test_item.py
@@ -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)
diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js
index 34d82ebc1f..639ae1e2ed 100644
--- a/cms/static/js/models/xblock_info.js
+++ b/cms/static/js/models/xblock_info.js
@@ -133,9 +133,19 @@ function(Backbone, _, str, ModuleUtils) {
*/
'has_content_group_components': null,
/**
- * Indicate the type of xblock
+ * actions defines the state of delete, drag and child add functionality for a xblock.
+ * currently, each xblock has default value of 'True' for keys: deletable, draggable and childAddable.
*/
- 'override_type': null
+ 'actions': null,
+ /**
+ * Header visible to UI.
+ */
+ 'is_header_visible': null,
+ /**
+ * Optional explanatory message about the xblock.
+ */
+ 'explanatory_message': null
+
},
initialize: function () {
@@ -172,13 +182,33 @@ function(Backbone, _, str, ModuleUtils) {
return !this.get('published') || this.get('has_changes');
},
- canBeDeleted: function(){
- //get the type of xblock
- if(this.get('override_type') != null) {
- var type = this.get('override_type');
+ isDeletable: function() {
+ return this.isActionRequired('deletable');
+ },
- //hide/remove the delete trash icon if type is entrance exam.
- if (_.has(type, 'is_entrance_exam') && type['is_entrance_exam']) {
+ isDraggable: function() {
+ return this.isActionRequired('draggable');
+ },
+
+ isChildAddable: function(){
+ return this.isActionRequired('childAddable');
+ },
+
+ isHeaderVisible: function(){
+ if(this.get('is_header_visible') !== null) {
+ return this.get('is_header_visible');
+ }
+ return true;
+ },
+
+ /**
+ * Return true if action is required e.g. delete, drag, add new child etc or if given key is not present.
+ * @return {boolean}
+ */
+ isActionRequired: function(actionName) {
+ var actions = this.get('actions');
+ if(actions !== null) {
+ if (_.has(actions, actionName) && !actions[actionName]) {
return false;
}
}
@@ -188,8 +218,8 @@ function(Backbone, _, str, ModuleUtils) {
/**
* Return a list of convenience methods to check affiliation to the category.
* @return {Array}
- */
- getCategoryHelpers: function () {
+ */
+ getCategoryHelpers: function () {
var categories = ['course', 'chapter', 'sequential', 'vertical'],
helpers = {};
@@ -200,15 +230,15 @@ function(Backbone, _, str, ModuleUtils) {
}, this);
return helpers;
- },
+ },
- /**
- * Check if we can edit current XBlock or not on Course Outline page.
- * @return {Boolean}
- */
- isEditableOnCourseOutline: function() {
- return this.isSequential() || this.isChapter() || this.isVertical();
- }
+ /**
+ * Check if we can edit current XBlock or not on Course Outline page.
+ * @return {Boolean}
+ */
+ isEditableOnCourseOutline: function() {
+ return this.isSequential() || this.isChapter() || this.isVertical();
+ }
});
return XBlockInfo;
});
diff --git a/cms/static/js/spec/models/xblock_info_spec.js b/cms/static/js/spec/models/xblock_info_spec.js
index c55c8baa7e..99a83f3f06 100644
--- a/cms/static/js/spec/models/xblock_info_spec.js
+++ b/cms/static/js/spec/models/xblock_info_spec.js
@@ -7,16 +7,47 @@ define(['backbone', 'js/models/xblock_info'],
expect(new XBlockInfo({'category': 'sequential'}).isEditableOnCourseOutline()).toBe(true);
expect(new XBlockInfo({'category': 'vertical'}).isEditableOnCourseOutline()).toBe(true);
});
+ });
- it('cannot delete an entrance exam', function(){
- expect(new XBlockInfo({'category': 'chapter', 'override_type': {'is_entrance_exam':true}})
- .canBeDeleted()).toBe(false);
+ describe('XblockInfo actions state and header visibility ', function() {
+
+ it('works correct to hide icons e.g. trash icon, drag when actions are not required', function(){
+ expect(new XBlockInfo({'category': 'chapter', 'actions': {'deletable':false}})
+ .isDeletable()).toBe(false);
+ expect(new XBlockInfo({'category': 'chapter', 'actions': {'draggable':false}})
+ .isDraggable()).toBe(false);
+ expect(new XBlockInfo({'category': 'chapter', 'actions': {'childAddable':false}})
+ .isChildAddable()).toBe(false);
});
- it('can delete module rather then entrance exam', function(){
- expect(new XBlockInfo({'category': 'chapter', 'override_type': {'is_entrance_exam':false}}).canBeDeleted()).toBe(true);
- expect(new XBlockInfo({'category': 'chapter', 'override_type': {}}).canBeDeleted()).toBe(true);
+ it('works correct to show icons e.g. trash icon, drag when actions are required', function(){
+ expect(new XBlockInfo({'category': 'chapter', 'actions': {'deletable':true}})
+ .isDeletable()).toBe(true);
+ expect(new XBlockInfo({'category': 'chapter', 'actions': {'draggable':true}})
+ .isDraggable()).toBe(true);
+ expect(new XBlockInfo({'category': 'chapter', 'actions': {'childAddable':true}})
+ .isChildAddable()).toBe(true);
});
+
+ it('displays icons e.g. trash icon, drag when actions are undefined', function(){
+ expect(new XBlockInfo({'category': 'chapter', 'actions': {}})
+ .isDeletable()).toBe(true);
+ expect(new XBlockInfo({'category': 'chapter', 'actions': {}})
+ .isDraggable()).toBe(true);
+ expect(new XBlockInfo({'category': 'chapter', 'actions': {}})
+ .isChildAddable()).toBe(true);
+ });
+
+ it('works correct to hide header content', function(){
+ expect(new XBlockInfo({'category': 'sequential', 'is_header_visible': false})
+ .isHeaderVisible()).toBe(false);
+ });
+
+ it('works correct to show header content when is_header_visible is not defined', function() {
+ expect(new XBlockInfo({'category': 'sequential', 'actions': {'deletable': true}})
+ .isHeaderVisible()).toBe(true);
+ });
+
});
}
);
diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js
index 4e0efc2d4d..98d55a0dd7 100644
--- a/cms/static/js/spec/views/pages/course_outline_spec.js
+++ b/cms/static/js/spec/views/pages/course_outline_spec.js
@@ -8,7 +8,7 @@ define(["jquery", "sinon", "js/common_helpers/ajax_helpers", "js/views/utils/vie
getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState,
collapseItemsAndVerifyState, createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON,
verifyTypePublishable, mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON,
- createMockVerticalJSON, createMockIndexJSON,
+ createMockVerticalJSON, createMockIndexJSON, mockCourseEntranceExamJSON
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'),
mockRerunNotification = readFixtures('mock/mock-course-rerun-notification.underscore');
@@ -228,6 +228,14 @@ define(["jquery", "sinon", "js/common_helpers/ajax_helpers", "js/views/utils/vie
mockSingleSectionCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON()
]);
+ mockCourseEntranceExamJSON = createMockCourseJSON({}, [
+ createMockSectionJSON({}, [
+ createMockSubsectionJSON({'is_header_visible': false}, [
+ createMockVerticalJSON()
+ ])
+ ])
+ ]);
+
});
afterEach(function () {
@@ -259,6 +267,11 @@ define(["jquery", "sinon", "js/common_helpers/ajax_helpers", "js/views/utils/vie
verifyItemsExpanded('subsection', false);
expect(getItemsOfType('unit')).not.toExist();
});
+
+ it('unit initially exist for entrance exam', function() {
+ createCourseOutlinePage(this, mockCourseEntranceExamJSON);
+ expect(getItemsOfType('unit')).toExist();
+ });
});
describe("Rerun notification", function () {
diff --git a/cms/static/js/views/xblock_outline.js b/cms/static/js/views/xblock_outline.js
index 7daaf3b34c..ade33c2bd0 100644
--- a/cms/static/js/views/xblock_outline.js
+++ b/cms/static/js/views/xblock_outline.js
@@ -44,6 +44,17 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
this.renderTemplate();
this.addButtonActions(this.$el);
this.addNameEditor();
+
+ // For cases in which we need to suppress the header controls during rendering, we'll
+ // need to add the current model's id/locator to the set of expanded locators
+ if (this.model.get('is_header_visible') !== null && !this.model.get('is_header_visible')) {
+ var locator = this.model.get('id');
+ if(!_.isUndefined(this.expandedLocators) && !this.expandedLocators.contains(locator)) {
+ this.expandedLocators.add(locator);
+ this.refresh();
+ }
+ }
+
if (this.shouldRenderChildren() && this.shouldExpandChildren()) {
this.renderChildren();
}
diff --git a/cms/static/sass/elements/_modules.scss b/cms/static/sass/elements/_modules.scss
index ef1eb3cdd3..889fbe8879 100644
--- a/cms/static/sass/elements/_modules.scss
+++ b/cms/static/sass/elements/_modules.scss
@@ -438,7 +438,8 @@ $outline-indent-width: $baseline;
}
// status - release
- .status-release {
+ .status-release,
+ .explanatory-message {
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.65;
}
@@ -463,7 +464,8 @@ $outline-indent-width: $baseline;
&:hover, &:active {
// status - release
- > .section-status .status-release {
+ > .section-status .status-release,
+ .section-status .explanatory-message{
opacity: 1.0;
}
}
diff --git a/cms/templates/js/course-outline.underscore b/cms/templates/js/course-outline.underscore
index 711b21e6ba..b9507426a2 100644
--- a/cms/templates/js/course-outline.underscore
+++ b/cms/templates/js/course-outline.underscore
@@ -40,7 +40,7 @@ if (xblockInfo.get('graded')) {
data-parent="<%= parentInfo.get('id') %>" data-locator="<%= xblockInfo.get('id') %>">
-
+ <% if (xblockInfo.isHeaderVisible()) { %>
<% if (!xblockInfo.isVertical()) { %>
-
+ <% if (xblockInfo.get('explanatory_message') !=null) { %>
+
+
+ <%= xblockInfo.get('explanatory_message') %>
+
+
+ <% } else { %>
+
<%= gettext('Release Status:') %>
@@ -116,7 +125,8 @@ if (xblockInfo.get('graded')) {
<% } %>
-
+
+ <% } %>
<% if (xblockInfo.get('due_date') || xblockInfo.get('graded')) { %>
@@ -138,6 +148,7 @@ if (xblockInfo.get('graded')) {
<% } %>
+ <% } %>
<% } %>
<% if (!parentInfo && xblockInfo.get('child_info') && xblockInfo.get('child_info').children.length === 0) { %>
@@ -159,6 +170,7 @@ if (xblockInfo.get('graded')) {
<% if (childType) { %>
+ <% if (xblockInfo.isChildAddable()) { %>
+ <% } %>
<% } %>
<% } %>
diff --git a/common/djangoapps/util/milestones_helpers.py b/common/djangoapps/util/milestones_helpers.py
index ef5f635783..18e808a0c3 100644
--- a/common/djangoapps/util/milestones_helpers.py
+++ b/common/djangoapps/util/milestones_helpers.py
@@ -21,6 +21,8 @@ from milestones.api import (
get_user_milestones,
)
from milestones.models import MilestoneRelationshipType
+from milestones.exceptions import InvalidMilestoneRelationshipTypeException
+from opaque_keys.edx.keys import UsageKey
NAMESPACE_CHOICES = {
'ENTRANCE_EXAM': 'entrance_exams'
@@ -150,6 +152,42 @@ def fulfill_course_milestone(course_key, user):
add_user_milestone({'id': user.id}, milestone)
+def get_required_content(course, user):
+ """
+ Queries milestones subsystem to see if the specified course is gated on one or more milestones,
+ and if those milestones can be fulfilled via completion of a particular course content module
+ """
+ required_content = []
+ if settings.FEATURES.get('MILESTONES_APP', False):
+ # Get all of the outstanding milestones for this course, for this user
+ try:
+ milestone_paths = get_course_milestones_fulfillment_paths(
+ unicode(course.id),
+ serialize_user(user)
+ )
+ except InvalidMilestoneRelationshipTypeException:
+ return required_content
+
+ # For each outstanding milestone, see if this content is one of its fulfillment paths
+ for path_key in milestone_paths:
+ milestone_path = milestone_paths[path_key]
+ if milestone_path.get('content') and len(milestone_path['content']):
+ for content in milestone_path['content']:
+ required_content.append(content)
+
+ #local imports to avoid circular reference
+ from student.models import EntranceExamConfiguration
+ can_skip_entrance_exam = EntranceExamConfiguration.user_can_skip_entrance_exam(user, course.id)
+ # check if required_content has any entrance exam and user is allowed to skip it
+ # then remove it from required content
+ if required_content and getattr(course, 'entrance_exam_enabled', False) and can_skip_entrance_exam:
+ descriptors = [modulestore().get_item(UsageKey.from_string(content)) for content in required_content]
+ entrance_exam_contents = [unicode(descriptor.location)
+ for descriptor in descriptors if descriptor.is_entrance_exam]
+ required_content = list(set(required_content) - set(entrance_exam_contents))
+ return required_content
+
+
def calculate_entrance_exam_score(user, course_descriptor, exam_modules):
"""
Calculates the score (percent) of the entrance exam using the provided modules
diff --git a/common/test/acceptance/tests/studio/test_studio_settings_details.py b/common/test/acceptance/tests/studio/test_studio_settings_details.py
index 1fc3f03561..b0f53858b7 100644
--- a/common/test/acceptance/tests/studio/test_studio_settings_details.py
+++ b/common/test/acceptance/tests/studio/test_studio_settings_details.py
@@ -162,3 +162,32 @@ class SettingsMilestonesTest(StudioCourseTest):
css_selector='span.section-title',
text='Entrance Exam'
))
+
+ def test_entrance_exam_has_unit_button(self):
+ """
+ Test that entrance exam should be created after checking the 'enable entrance exam' checkbox.
+ And user has option to add units only instead of any Subsection.
+ """
+ self.settings_detail.require_entrance_exam(required=True)
+ self.settings_detail.save_changes()
+
+ # getting the course outline page.
+ course_outline_page = CourseOutlinePage(
+ self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']
+ )
+ course_outline_page.visit()
+ course_outline_page.wait_for_ajax()
+
+ # button with text 'New Unit' should be present.
+ self.assertTrue(element_has_text(
+ page=course_outline_page,
+ css_selector='.add-item a.button-new',
+ text='New Unit'
+ ))
+
+ # button with text 'New Subsection' should not be present.
+ self.assertFalse(element_has_text(
+ page=course_outline_page,
+ css_selector='.add-item a.button-new',
+ text='New Subsection'
+ ))
diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py
index 9a4f778c0a..bc6c64ca37 100644
--- a/lms/djangoapps/courseware/courses.py
+++ b/lms/djangoapps/courseware/courses.py
@@ -9,7 +9,7 @@ from django.conf import settings
from edxmako.shortcuts import render_to_string
from xmodule.modulestore import ModuleStoreEnum
-from opaque_keys.edx.keys import CourseKey, UsageKey
+from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent
from xmodule.modulestore.exceptions import ItemNotFoundError
@@ -23,6 +23,10 @@ from courseware.model_data import FieldDataCache
from courseware.module_render import get_module
from student.models import CourseEnrollment
import branding
+from util.milestones_helpers import get_required_content, calculate_entrance_exam_score
+from util.module_utils import yield_dynamic_descriptor_descendents
+from opaque_keys.edx.keys import UsageKey
+from .module_render import get_module_for_descriptor
log = logging.getLogger(__name__)
@@ -441,3 +445,47 @@ def get_problems_in_section(section):
problem_descriptors[unicode(component.location)] = component
return problem_descriptors
+
+
+def get_entrance_exam_score(request, course):
+ """
+ Get entrance exam score
+ """
+ exam_key = UsageKey.from_string(course.entrance_exam_id)
+ exam_descriptor = modulestore().get_item(exam_key)
+
+ def inner_get_module(descriptor):
+ """
+ Delegate to get_module_for_descriptor.
+ """
+ field_data_cache = FieldDataCache([descriptor], course.id, request.user)
+ return get_module_for_descriptor(request.user, request, descriptor, field_data_cache, course.id)
+
+ exam_module_generators = yield_dynamic_descriptor_descendents(
+ exam_descriptor,
+ inner_get_module
+ )
+ exam_modules = [module for module in exam_module_generators]
+ return calculate_entrance_exam_score(request.user, course, exam_modules)
+
+
+def get_entrance_exam_content_info(request, course):
+ """
+ Get the entrance exam content information e.g. chapter, exam passing state.
+ return exam chapter and its passing state.
+ """
+ required_content = get_required_content(course, request.user)
+ exam_chapter = None
+ is_exam_passed = True
+ # Iterating the list of required content of this course.
+ for content in required_content:
+ # database lookup to required content pointer
+ usage_key = course.id.make_usage_key_from_deprecated_string(content)
+ module_item = modulestore().get_item(usage_key)
+ if not module_item.hide_from_toc and module_item.is_entrance_exam:
+ # Here we are looking for entrance exam module/chapter in required_content.
+ # If module_item is an entrance exam chapter then set and return its info e.g. exam chapter, exam state.
+ exam_chapter = module_item
+ is_exam_passed = False
+ break
+ return exam_chapter, is_exam_passed
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 3fe94592a2..f6dd10ad7a 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -36,7 +36,7 @@ from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from edxmako.shortcuts import render_to_string
from eventtracking import tracker
from psychometrics.psychoanalyze import make_psychometrics_data_update_handler
-from student.models import anonymous_id_for_user, user_by_anonymous_id, EntranceExamConfiguration
+from student.models import anonymous_id_for_user, user_by_anonymous_id
from xblock.core import XBlock
from xblock.fields import Scope
from xblock.runtime import KvsFieldData, KeyValueStore
@@ -65,8 +65,7 @@ from util.json_request import JsonResponse
from util.sandboxing import can_execute_unsafe_code, get_python_lib_zip
if settings.FEATURES.get('MILESTONES_APP', False):
from milestones import api as milestones_api
- from milestones.exceptions import InvalidMilestoneRelationshipTypeException
- from util.milestones_helpers import serialize_user, calculate_entrance_exam_score
+ from util.milestones_helpers import calculate_entrance_exam_score, get_required_content
from util.module_utils import yield_dynamic_descriptor_descendents
log = logging.getLogger(__name__)
@@ -107,40 +106,6 @@ def make_track_function(request):
return function
-def _get_required_content(course, user):
- """
- Queries milestones subsystem to see if the specified course is gated on one or more milestones,
- and if those milestones can be fulfilled via completion of a particular course content module
- """
- required_content = []
- if settings.FEATURES.get('MILESTONES_APP', False):
- # Get all of the outstanding milestones for this course, for this user
- try:
- milestone_paths = milestones_api.get_course_milestones_fulfillment_paths(
- unicode(course.id),
- serialize_user(user)
- )
- except InvalidMilestoneRelationshipTypeException:
- return required_content
-
- # For each outstanding milestone, see if this content is one of its fulfillment paths
- for path_key in milestone_paths:
- milestone_path = milestone_paths[path_key]
- if milestone_path.get('content') and len(milestone_path['content']):
- for content in milestone_path['content']:
- required_content.append(content)
-
- can_skip_entrance_exam = EntranceExamConfiguration.user_can_skip_entrance_exam(user, course.id)
- # check if required_content has any entrance exam and user is allowed to skip it
- # then remove it from required content
- if required_content and getattr(course, 'entrance_exam_enabled', False) and can_skip_entrance_exam:
- descriptors = [modulestore().get_item(UsageKey.from_string(content)) for content in required_content]
- entrance_exam_contents = [unicode(descriptor.location)
- for descriptor in descriptors if descriptor.is_entrance_exam]
- required_content = list(set(required_content) - set(entrance_exam_contents))
- return required_content
-
-
def toc_for_course(request, course, active_chapter, active_section, field_data_cache):
'''
Create a table of contents from the module store
@@ -170,8 +135,8 @@ def toc_for_course(request, course, active_chapter, active_section, field_data_c
if course_module is None:
return None
- # Check to see if the course is gated on required content (such as an Entrance Exam)
- required_content = _get_required_content(course, request.user)
+ # Check to see if the course is gated on milestone-required content (such as an Entrance Exam)
+ required_content = get_required_content(course, request.user)
chapters = list()
for chapter in course_module.get_display_items():
diff --git a/lms/djangoapps/courseware/tests/test_entrance_exam.py b/lms/djangoapps/courseware/tests/test_entrance_exam.py
index ff4c7073ca..1bdb0ca278 100644
--- a/lms/djangoapps/courseware/tests/test_entrance_exam.py
+++ b/lms/djangoapps/courseware/tests/test_entrance_exam.py
@@ -8,12 +8,16 @@ from django.core.urlresolvers import reverse
from courseware.model_data import FieldDataCache
from courseware.module_render import get_module, toc_for_course
from courseware.tests.factories import UserFactory, InstructorFactory
+from courseware.courses import get_entrance_exam_content_info, get_entrance_exam_score
from milestones import api as milestones_api
from milestones.models import MilestoneRelationshipType
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, TEST_DATA_MOCK_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from util.milestones_helpers import generate_milestone_namespace, NAMESPACE_CHOICES
+from student.models import CourseEnrollment
+from mock import patch
+import mock
class EntranceExamTestCases(ModuleStoreTestCase):
@@ -31,12 +35,12 @@ class EntranceExamTestCases(ModuleStoreTestCase):
'entrance_exam_enabled': True,
}
)
- chapter = ItemFactory.create(
+ self.chapter = ItemFactory.create(
parent=self.course,
display_name='Overview'
)
ItemFactory.create(
- parent=chapter,
+ parent=self.chapter,
display_name='Welcome'
)
ItemFactory.create(
@@ -44,11 +48,27 @@ class EntranceExamTestCases(ModuleStoreTestCase):
category='chapter',
display_name="Week 1"
)
- ItemFactory.create(
- parent=chapter,
+ self.chapter_subsection = ItemFactory.create(
+ parent=self.chapter,
category='sequential',
display_name="Lesson 1"
)
+ chapter_vertical = ItemFactory.create(
+ parent=self.chapter_subsection,
+ category='vertical',
+ display_name='Lesson 1 Vertical - Unit 1'
+ )
+ ItemFactory.create(
+ parent=chapter_vertical,
+ category="problem",
+ display_name="Problem - Unit 1 Problem 1"
+ )
+ ItemFactory.create(
+ parent=chapter_vertical,
+ category="problem",
+ display_name="Problem - Unit 1 Problem 2"
+ )
+
ItemFactory.create(
category="instructor",
parent=self.course,
@@ -59,7 +79,8 @@ class EntranceExamTestCases(ModuleStoreTestCase):
parent=self.course,
category="chapter",
display_name="Entrance Exam Section - Chapter 1",
- is_entrance_exam=True
+ is_entrance_exam=True,
+ in_entrance_exam=True
)
self.exam_1 = ItemFactory.create(
parent=self.entrance_exam,
@@ -125,13 +146,14 @@ class EntranceExamTestCases(ModuleStoreTestCase):
user,
self.entrance_exam
)
- self.entrance_exam.is_entrance_exam = True
- self.entrance_exam.in_entrance_exam = True
self.course.entrance_exam_enabled = True
self.course.entrance_exam_minimum_score_pct = 0.50
self.course.entrance_exam_id = unicode(self.entrance_exam.scope_ids.usage_id)
modulestore().update_item(self.course, user.id) # pylint: disable=no-member
+ self.client.login(username=self.request.user.username, password="test")
+ CourseEnrollment.enroll(self.request.user, self.course.id)
+
self.expected_locked_toc = (
[
{
@@ -206,6 +228,186 @@ class EntranceExamTestCases(ModuleStoreTestCase):
]
)
+ @mock.patch('xmodule.x_module.XModuleMixin.has_dynamic_children', mock.Mock(return_value='True'))
+ def test_view_redirect_if_entrance_exam_required(self):
+ """
+ Unit Test: if entrance exam is required. Should return a redirect.
+ """
+ url = reverse('courseware', kwargs={'course_id': unicode(self.course.id)})
+ expected_url = reverse('courseware_section',
+ kwargs={
+ 'course_id': unicode(self.course.id),
+ 'chapter': self.entrance_exam.location.name,
+ 'section': self.exam_1.location.name
+ })
+ resp = self.client.get(url)
+ self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
+
+ @patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': False})
+ def test_entrance_exam_content_absence(self):
+ """
+ Unit Test: If entrance exam is not enabled then page should be redirected with chapter contents.
+ """
+ url = reverse('courseware', kwargs={'course_id': unicode(self.course.id)})
+ expected_url = reverse('courseware_section',
+ kwargs={
+ 'course_id': unicode(self.course.id),
+ 'chapter': self.chapter.location.name,
+ 'section': self.chapter_subsection.location.name
+ })
+ resp = self.client.get(url)
+ self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
+ resp = self.client.get(expected_url)
+ self.assertNotIn('Exam Problem - Problem 1', resp.content)
+ self.assertNotIn('Exam Problem - Problem 2', resp.content)
+
+ @patch.dict('django.conf.settings.FEATURES', {'ENTRANCE_EXAMS': True})
+ def test_entrance_exam_content_presence(self):
+ """
+ Unit Test: If entrance exam is enabled then its content e.g. problems should be loaded and redirection will
+ occur with entrance exam contents.
+ """
+ url = reverse('courseware', kwargs={'course_id': unicode(self.course.id)})
+ expected_url = reverse('courseware_section',
+ kwargs={
+ 'course_id': unicode(self.course.id),
+ 'chapter': self.entrance_exam.location.name,
+ 'section': self.exam_1.location.name
+ })
+ resp = self.client.get(url)
+ self.assertRedirects(resp, expected_url, status_code=302, target_status_code=200)
+ resp = self.client.get(expected_url)
+ self.assertIn('Exam Problem - Problem 1', resp.content)
+ self.assertIn('Exam Problem - Problem 2', resp.content)
+
+ def test_entrance_exam_content_info(self):
+ """
+ test entrance exam content info method
+ """
+ exam_chapter, is_exam_passed = get_entrance_exam_content_info(self.request, self.course)
+ self.assertEqual(exam_chapter.url_name, self.entrance_exam.url_name)
+ self.assertEqual(is_exam_passed, False)
+
+ # Pass the entrance exam
+ # pylint: disable=maybe-no-member,no-member
+ grade_dict = {'value': 1, 'max_value': 1, 'user_id': self.request.user.id}
+ field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
+ self.course.id,
+ self.request.user,
+ self.course,
+ depth=2
+ )
+ # pylint: disable=protected-access
+ module = get_module(
+ self.request.user,
+ self.request,
+ self.problem_1.scope_ids.usage_id,
+ field_data_cache,
+ )._xmodule
+ module.system.publish(self.problem_1, 'grade', grade_dict)
+
+ exam_chapter, is_exam_passed = get_entrance_exam_content_info(self.request, self.course)
+ self.assertEqual(exam_chapter, None)
+ self.assertEqual(is_exam_passed, True)
+
+ def test_entrance_exam_score(self):
+ """
+ test entrance exam score. we will hit the method get_entrance_exam_score to verify exam score.
+ """
+ exam_score = get_entrance_exam_score(self.request, self.course)
+ self.assertEqual(exam_score, 0)
+
+ # Pass the entrance exam
+ # pylint: disable=maybe-no-member,no-member
+ grade_dict = {'value': 1, 'max_value': 2, 'user_id': self.request.user.id}
+ field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
+ self.course.id,
+ self.request.user,
+ self.course,
+ depth=2
+ )
+ # pylint: disable=protected-access
+ module = get_module(
+ self.request.user,
+ self.request,
+ self.problem_1.scope_ids.usage_id,
+ field_data_cache,
+ )._xmodule
+ module.system.publish(self.problem_1, 'grade', grade_dict)
+
+ exam_score = get_entrance_exam_score(self.request, self.course)
+ # 50 percent exam score should be achieved.
+ self.assertEqual(exam_score * 100, 50)
+
+ def test_entrance_exam_requirement_message(self):
+ """
+ Unit Test: entrance exam requirement message should be present in response
+ """
+ url = reverse(
+ 'courseware_section',
+ kwargs={
+ 'course_id': unicode(self.course.id),
+ 'chapter': self.entrance_exam.location.name,
+ 'section': self.exam_1.location.name
+ }
+ )
+ resp = self.client.get(url)
+ self.assertEqual(resp.status_code, 200)
+ self.assertIn('To access course materials, you must score', resp.content)
+
+ def test_entrance_exam_requirement_message_hidden(self):
+ """
+ Unit Test: entrance exam message should not be present outside the context of entrance exam subsection.
+ """
+ url = reverse(
+ 'courseware_section',
+ kwargs={
+ 'course_id': unicode(self.course.id),
+ 'chapter': self.chapter.location.name,
+ 'section': self.chapter_subsection.location.name
+ }
+ )
+ resp = self.client.get(url)
+ self.assertEqual(resp.status_code, 200)
+ self.assertNotIn('To access course materials, you must score', resp.content)
+ self.assertNotIn('You have passed the entrance exam.', resp.content)
+
+ def test_entrance_exam_passed_message_and_course_content(self):
+ """
+ Unit Test: exam passing message and rest of the course section should be present
+ when user achieves the entrance exam milestone/pass the exam.
+ """
+ url = reverse(
+ 'courseware_section',
+ kwargs={
+ 'course_id': unicode(self.course.id),
+ 'chapter': self.entrance_exam.location.name,
+ 'section': self.exam_1.location.name
+ }
+ )
+
+ # pylint: disable=maybe-no-member,no-member
+ grade_dict = {'value': 1, 'max_value': 1, 'user_id': self.request.user.id}
+ field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
+ self.course.id,
+ self.request.user,
+ self.course,
+ depth=2
+ )
+ # pylint: disable=protected-access
+ module = get_module(
+ self.request.user,
+ self.request,
+ self.problem_1.scope_ids.usage_id,
+ field_data_cache,
+ )._xmodule
+ module.system.publish(self.problem_1, 'grade', grade_dict)
+
+ resp = self.client.get(url)
+ self.assertNotIn('To access course materials, you must score', resp.content)
+ self.assertIn('You have passed the entrance exam.', resp.content)
+ self.assertIn('Lesson 1', resp.content)
+
def test_entrance_exam_gating(self):
"""
Unit Test: test_entrance_exam_gating
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index e960177285..911283959f 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -33,8 +33,9 @@ from markupsafe import escape
from courseware import grades
from courseware.access import has_access, _adjust_start_date_for_beta_testers
-from courseware.courses import get_courses, get_course, get_studio_url, get_course_with_access, sort_by_announcement
-from courseware.courses import sort_by_start_date
+from courseware.courses import get_courses, get_course, get_studio_url, get_course_with_access, sort_by_announcement,\
+ get_entrance_exam_content_info
+from courseware.courses import sort_by_start_date, get_entrance_exam_score
from courseware.masquerade import setup_masquerade
from courseware.model_data import FieldDataCache
from .module_render import toc_for_course, get_module_for_descriptor, get_module
@@ -411,6 +412,19 @@ def _index_bulk_op(request, course_key, chapter, section, position):
# Show empty courseware for a course with no units
return render_to_response('courseware/courseware.html', context)
elif chapter is None:
+ # Check first to see if we should instead redirect the user to an Entrance Exam
+ if settings.FEATURES.get('ENTRANCE_EXAMS', False) and course.entrance_exam_enabled:
+ exam_chapter, __ = get_entrance_exam_content_info(request, course)
+ if exam_chapter is not None:
+ exam_section = None
+ if exam_chapter.get_children():
+ exam_section = exam_chapter.get_children()[0]
+ if exam_section:
+ return redirect('courseware_section',
+ course_id=unicode(course_key),
+ chapter=exam_chapter.url_name,
+ section=exam_section.url_name)
+
# passing CONTENT_DEPTH avoids returning 404 for a course with an
# empty first section and a second section with content
return redirect_to_course_position(course_module, CONTENT_DEPTH)
@@ -441,6 +455,14 @@ def _index_bulk_op(request, course_key, chapter, section, position):
return redirect(reverse('courseware', args=[course.id.to_deprecated_string()]))
raise Http404
+ if settings.FEATURES.get('ENTRANCE_EXAMS', False) and course.entrance_exam_enabled:
+ # Message should not appear outside the context of entrance exam subsection.
+ # if section is none then we don't need to show message on welcome back screen also.
+ if getattr(chapter_module, 'is_entrance_exam', False) and section is not None:
+ __, is_exam_passed = get_entrance_exam_content_info(request, course)
+ context['entrance_exam_current_score'] = get_entrance_exam_score(request, course)
+ context['entrance_exam_passed'] = is_exam_passed
+
if section is not None:
section_descriptor = chapter_descriptor.get_child_by(lambda m: m.location.name == section)
diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss
index 4f8470891c..ddaad0875c 100644
--- a/lms/static/sass/course/courseware/_courseware.scss
+++ b/lms/static/sass/course/courseware/_courseware.scss
@@ -60,6 +60,14 @@ div.course-wrapper {
}
}
+ .sequential-status-message {
+ margin-bottom: $baseline;
+ background-color: $gray-l5;
+ padding: ($baseline * 0.75);
+ border-radius: 3px;
+ @include font-size(13);
+ }
+
ul {
li {
margin-bottom: lh(0.5);
diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html
index cc122cc653..a4e81911e2 100644
--- a/lms/templates/courseware/courseware.html
+++ b/lms/templates/courseware/courseware.html
@@ -234,6 +234,26 @@ ${fragment.foot_html()}
% endif
+ % if getattr(course, 'entrance_exam_enabled') and \
+ getattr(course, 'entrance_exam_minimum_score_pct') and \
+ entrance_exam_current_score is not UNDEFINED:
+ % if not entrance_exam_passed:
+
+ ${_('To access course materials, you must score {required_score}% or higher on this \
+ exam. Your current score is {current_score}%.').format(
+ required_score=int(course.entrance_exam_minimum_score_pct * 100),
+ current_score=int(entrance_exam_current_score * 100)
+ )}
+
+ % else:
+
+ ${_('Your score is {current_score}%. You have passed the entrance exam.').format(
+ current_score=int(entrance_exam_current_score * 100)
+ )}
+
+ % endif
+ % endif
+
${fragment.body_html()}
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):