Merge pull request #6997 from edx/asadiqbal08/merge_SOL-199_and_SOL-221
Combine the changes of SOL-199 and SOL-221
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ if (xblockInfo.get('graded')) {
|
||||
data-parent="<%= parentInfo.get('id') %>" data-locator="<%= xblockInfo.get('id') %>">
|
||||
|
||||
<span class="draggable-drop-indicator draggable-drop-indicator-before"><i class="icon fa fa-caret-right"></i></span>
|
||||
|
||||
<% if (xblockInfo.isHeaderVisible()) { %>
|
||||
<div class="<%= xblockType %>-header">
|
||||
<% if (includesChildren) { %>
|
||||
<h3 class="<%= xblockType %>-header-details expand-collapse <%= isCollapsed ? 'expand' : 'collapse' %> ui-toggle-expansion"
|
||||
@@ -78,7 +78,7 @@ if (xblockInfo.get('graded')) {
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
<% if (xblockInfo.canBeDeleted()) { %>
|
||||
<% if (xblockInfo.isDeletable()) { %>
|
||||
<li class="action-item action-delete">
|
||||
<a href="#" data-tooltip="<%= gettext('Delete') %>" class="delete-button action-button">
|
||||
<i class="icon fa fa-trash-o" aria-hidden="true"></i>
|
||||
@@ -86,18 +86,27 @@ if (xblockInfo.get('graded')) {
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
<% if (xblockInfo.isDraggable()) { %>
|
||||
<li class="action-item action-drag">
|
||||
<span data-tooltip="<%= gettext('Drag to reorder') %>"
|
||||
class="drag-handle <%= xblockType %>-drag-handle action">
|
||||
<span class="sr"><%= gettext('Drag to reorder') %></span>
|
||||
</span>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="<%= xblockType %>-status">
|
||||
<% if (!xblockInfo.isVertical()) { %>
|
||||
<div class="status-release">
|
||||
<% if (xblockInfo.get('explanatory_message') !=null) { %>
|
||||
<div class="explanatory-message">
|
||||
<span>
|
||||
<%= xblockInfo.get('explanatory_message') %>
|
||||
</span>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="status-release">
|
||||
<p>
|
||||
<span class="sr status-release-label"><%= gettext('Release Status:') %></span>
|
||||
<span class="status-release-value">
|
||||
@@ -116,7 +125,8 @@ if (xblockInfo.get('graded')) {
|
||||
<% } %>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (xblockInfo.get('due_date') || xblockInfo.get('graded')) { %>
|
||||
<div class="status-grading">
|
||||
<p>
|
||||
@@ -138,6 +148,7 @@ if (xblockInfo.get('graded')) {
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<% if (!parentInfo && xblockInfo.get('child_info') && xblockInfo.get('child_info').children.length === 0) { %>
|
||||
@@ -159,6 +170,7 @@ if (xblockInfo.get('graded')) {
|
||||
</ol>
|
||||
|
||||
<% if (childType) { %>
|
||||
<% if (xblockInfo.isChildAddable()) { %>
|
||||
<div class="add-<%= childType %> add-item">
|
||||
<a href="#" class="button button-new" data-category="<%= childCategory %>"
|
||||
data-parent="<%= xblockInfo.get('id') %>" data-default-name="<%= defaultNewChildName %>"
|
||||
@@ -166,6 +178,7 @@ if (xblockInfo.get('graded')) {
|
||||
<i class="icon fa fa-plus"></i><%= addChildLabel %>
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -234,6 +234,26 @@ ${fragment.foot_html()}
|
||||
</div>
|
||||
% endif
|
||||
<section class="course-content" id="course-content">
|
||||
% 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:
|
||||
<p class="sequential-status-message">
|
||||
${_('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)
|
||||
)}
|
||||
</p>
|
||||
% else:
|
||||
<p class="sequential-status-message">
|
||||
${_('Your score is {current_score}%. You have passed the entrance exam.').format(
|
||||
current_score=int(entrance_exam_current_score * 100)
|
||||
)}
|
||||
</p>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
${fragment.body_html()}
|
||||
</section>
|
||||
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
|
||||
|
||||
Reference in New Issue
Block a user