Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
918b184e
Commit
918b184e
authored
Mar 10, 2015
by
Matt Drayer
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #6997 from edx/asadiqbal08/merge_SOL-199_and_SOL-221
Combine the changes of SOL-199 and SOL-221
parents
57c38649
5a7ac441
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
669 additions
and
102 deletions
+669
-102
cms/djangoapps/contentstore/tests/test_course_settings.py
+15
-1
cms/djangoapps/contentstore/views/course.py
+24
-11
cms/djangoapps/contentstore/views/entrance_exam.py
+37
-3
cms/djangoapps/contentstore/views/item.py
+19
-5
cms/djangoapps/contentstore/views/tests/test_item.py
+62
-2
cms/static/js/models/xblock_info.js
+48
-18
cms/static/js/spec/models/xblock_info_spec.js
+37
-6
cms/static/js/spec/views/pages/course_outline_spec.js
+14
-1
cms/static/js/views/xblock_outline.js
+11
-0
cms/static/sass/elements/_modules.scss
+4
-2
cms/templates/js/course-outline.underscore
+17
-4
common/djangoapps/util/milestones_helpers.py
+38
-0
common/test/acceptance/tests/studio/test_studio_settings_details.py
+29
-0
lms/djangoapps/courseware/courses.py
+49
-1
lms/djangoapps/courseware/module_render.py
+4
-39
lms/djangoapps/courseware/tests/test_entrance_exam.py
+209
-7
lms/djangoapps/courseware/views.py
+24
-2
lms/static/sass/course/courseware/_courseware.scss
+8
-0
lms/templates/courseware/courseware.html
+20
-0
No files found.
cms/djangoapps/contentstore/tests/test_course_settings.py
View file @
918b184e
...
...
@@ -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
(
...
...
cms/djangoapps/contentstore/views/course.py
View file @
918b184e
...
...
@@ -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...
...
...
cms/djangoapps/contentstore/views/entrance_exam.py
View file @
918b184e
...
...
@@ -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
...
...
cms/djangoapps/contentstore/views/item.py
View file @
918b184e
...
...
@@ -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
:
...
...
cms/djangoapps/contentstore/views/tests/test_item.py
View file @
918b184e
...
...
@@ -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
)
...
...
cms/static/js/models/xblock_info.js
View file @
918b184e
...
...
@@ -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'
);
},
isDraggable
:
function
()
{
return
this
.
isActionRequired
(
'draggable'
);
},
//hide/remove the delete trash icon if type is entrance exam.
if
(
_
.
has
(
type
,
'is_entrance_exam'
)
&&
type
[
'is_entrance_exam'
])
{
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
;
});
cms/static/js/spec/models/xblock_info_spec.js
View file @
918b184e
...
...
@@ -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
);
});
});
describe
(
'XblockInfo actions state and header visibility '
,
function
()
{
it
(
'cannot delete an entrance exam'
,
function
(){
expect
(
new
XBlockInfo
({
'category'
:
'chapter'
,
'override_type'
:
{
'is_entrance_exam'
:
true
}})
.
canBeDeleted
()).
toBe
(
false
);
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
);
});
});
}
);
cms/static/js/spec/views/pages/course_outline_spec.js
View file @
918b184e
...
...
@@ -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
()
{
...
...
cms/static/js/views/xblock_outline.js
View file @
918b184e
...
...
@@ -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
();
}
...
...
cms/static/sass/elements/_modules.scss
View file @
918b184e
...
...
@@ -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
;
}
}
...
...
cms/templates/js/course-outline.underscore
View file @
918b184e
...
...
@@ -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>
<% } %>
...
...
common/djangoapps/util/milestones_helpers.py
View file @
918b184e
...
...
@@ -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
...
...
common/test/acceptance/tests/studio/test_studio_settings_details.py
View file @
918b184e
...
...
@@ -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'
))
lms/djangoapps/courseware/courses.py
View file @
918b184e
...
...
@@ -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
lms/djangoapps/courseware/module_render.py
View file @
918b184e
...
...
@@ -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
():
...
...
lms/djangoapps/courseware/tests/test_entrance_exam.py
View file @
918b184e
...
...
@@ -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
...
...
lms/djangoapps/courseware/views.py
View file @
918b184e
...
...
@@ -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
)
...
...
lms/static/sass/course/courseware/_courseware.scss
View file @
918b184e
...
...
@@ -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
);
...
...
lms/templates/courseware/courseware.html
View file @
918b184e
...
...
@@ -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'):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment