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
1c839cc0
Commit
1c839cc0
authored
Jan 16, 2015
by
Matt Drayer
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Addressed several outstanding issues related to initial entrance exams feature delivery
parent
495583ac
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
213 additions
and
125 deletions
+213
-125
cms/djangoapps/contentstore/views/entrance_exam.py
+12
-7
cms/djangoapps/contentstore/views/helpers.py
+108
-0
cms/djangoapps/contentstore/views/item.py
+14
-98
cms/djangoapps/contentstore/views/tests/test_item.py
+14
-0
cms/static/js/spec/models/xblock_info_spec.js
+10
-0
cms/static/js/spec/views/settings/main_spec.js
+54
-19
common/lib/xmodule/xmodule/seq_module.py
+1
-1
No files found.
cms/djangoapps/contentstore/views/entrance_exam.py
View file @
1c839cc0
...
...
@@ -10,7 +10,8 @@ from django_future.csrf import ensure_csrf_cookie
from
django.http
import
HttpResponse
from
django.test
import
RequestFactory
from
contentstore.views.item
import
create_item
,
delete_item
from
contentstore.views.helpers
import
create_xblock
from
contentstore.views.item
import
delete_item
from
milestones
import
api
as
milestones_api
from
models.settings.course_metadata
import
CourseMetadata
from
opaque_keys.edx.keys
import
CourseKey
,
UsageKey
...
...
@@ -108,10 +109,14 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N
'is_entrance_exam'
:
True
,
'in_entrance_exam'
:
True
,
}
factory
=
RequestFactory
()
internal_request
=
factory
.
post
(
'/'
,
json
.
dumps
(
payload
),
content_type
=
"application/json"
)
internal_request
.
user
=
request
.
user
created_item
=
json
.
loads
(
create_item
(
internal_request
)
.
content
)
parent_locator
=
unicode
(
course
.
location
)
created_block
=
create_xblock
(
parent_locator
=
parent_locator
,
user
=
request
.
user
,
category
=
'chapter'
,
display_name
=
'Entrance Exam'
,
is_entrance_exam
=
True
)
# Set the entrance exam metadata flags for this course
# Reload the course so we don't overwrite the new child reference
...
...
@@ -119,7 +124,7 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N
metadata
=
{
'entrance_exam_enabled'
:
True
,
'entrance_exam_minimum_score_pct'
:
entrance_exam_minimum_score_pct
/
100
,
'entrance_exam_id'
:
created_item
[
'locator'
]
,
'entrance_exam_id'
:
unicode
(
created_block
.
location
)
,
}
CourseMetadata
.
update_from_dict
(
metadata
,
course
,
request
.
user
)
...
...
@@ -146,7 +151,7 @@ def _create_entrance_exam(request, course_key, entrance_exam_minimum_score_pct=N
)
milestones_api
.
add_course_content_milestone
(
unicode
(
course
.
id
),
created_item
[
'locator'
]
,
unicode
(
created_block
.
location
)
,
relationship_types
[
'FULFILLS'
],
milestone
)
...
...
cms/djangoapps/contentstore/views/helpers.py
View file @
1c839cc0
...
...
@@ -4,16 +4,22 @@ Helper methods for Studio views.
from
__future__
import
absolute_import
from
uuid
import
uuid4
import
urllib
from
django.conf
import
settings
from
django.http
import
HttpResponse
from
django.shortcuts
import
redirect
from
django.utils.translation
import
ugettext
as
_
from
edxmako.shortcuts
import
render_to_string
,
render_to_response
from
opaque_keys.edx.keys
import
UsageKey
from
xblock.core
import
XBlock
from
xmodule.modulestore.django
import
modulestore
from
xmodule.tabs
import
StaticTab
from
contentstore.utils
import
reverse_course_url
,
reverse_library_url
,
reverse_usage_url
from
models.settings.course_grading
import
CourseGradingModel
__all__
=
[
'edge'
,
'event'
,
'landing'
]
...
...
@@ -154,3 +160,105 @@ def xblock_primary_child_category(xblock):
elif
category
==
'sequential'
:
return
'vertical'
return
None
def
usage_key_with_run
(
usage_key_string
):
"""
Converts usage_key_string to a UsageKey, adding a course run if necessary
"""
usage_key
=
UsageKey
.
from_string
(
usage_key_string
)
usage_key
=
usage_key
.
replace
(
course_key
=
modulestore
()
.
fill_in_run
(
usage_key
.
course_key
))
return
usage_key
def
create_xblock
(
parent_locator
,
user
,
category
,
display_name
,
boilerplate
=
None
,
is_entrance_exam
=
False
):
"""
Performs the actual grunt work of creating items/xblocks -- knows nothing about requests, views, etc.
"""
store
=
modulestore
()
usage_key
=
usage_key_with_run
(
parent_locator
)
with
store
.
bulk_operations
(
usage_key
.
course_key
):
parent
=
store
.
get_item
(
usage_key
)
dest_usage_key
=
usage_key
.
replace
(
category
=
category
,
name
=
uuid4
()
.
hex
)
# get the metadata, display_name, and definition from the caller
metadata
=
{}
data
=
None
template_id
=
boilerplate
if
template_id
:
clz
=
parent
.
runtime
.
load_block_type
(
category
)
if
clz
is
not
None
:
template
=
clz
.
get_template
(
template_id
)
if
template
is
not
None
:
metadata
=
template
.
get
(
'metadata'
,
{})
data
=
template
.
get
(
'data'
)
if
display_name
is
not
None
:
metadata
[
'display_name'
]
=
display_name
# We should use the 'fields' kwarg for newer module settings/values (vs. metadata or data)
fields
=
{}
# Entrance Exams: Chapter module positioning
child_position
=
None
if
settings
.
FEATURES
.
get
(
'ENTRANCE_EXAMS'
,
False
):
if
category
==
'chapter'
and
is_entrance_exam
:
fields
[
'is_entrance_exam'
]
=
is_entrance_exam
fields
[
'in_entrance_exam'
]
=
True
# Inherited metadata, all children will have it
child_position
=
0
# TODO need to fix components that are sending definition_data as strings, instead of as dicts
# For now, migrate them into dicts here.
if
isinstance
(
data
,
basestring
):
data
=
{
'data'
:
data
}
created_block
=
store
.
create_child
(
user
.
id
,
usage_key
,
dest_usage_key
.
block_type
,
block_id
=
dest_usage_key
.
block_id
,
fields
=
fields
,
definition_data
=
data
,
metadata
=
metadata
,
runtime
=
parent
.
runtime
,
position
=
child_position
,
)
# Entrance Exams: Grader assignment
if
settings
.
FEATURES
.
get
(
'ENTRANCE_EXAMS'
,
False
):
course
=
store
.
get_course
(
usage_key
.
course_key
)
if
hasattr
(
course
,
'entrance_exam_enabled'
)
and
course
.
entrance_exam_enabled
:
if
category
==
'sequential'
and
parent_locator
==
course
.
entrance_exam_id
:
grader
=
{
"type"
:
"Entrance Exam"
,
"min_count"
:
0
,
"drop_count"
:
0
,
"short_label"
:
"Entrance"
,
"weight"
:
0
}
grading_model
=
CourseGradingModel
.
update_grader_from_json
(
course
.
id
,
grader
,
user
)
CourseGradingModel
.
update_section_grader_type
(
created_block
,
grading_model
[
'type'
],
user
)
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
# if we add one then we need to also add it to the policy information (i.e. metadata)
# we should remove this once we can break this reference from the course to static tabs
if
category
==
'static_tab'
:
display_name
=
display_name
or
_
(
"Empty"
)
# Prevent name being None
course
=
store
.
get_course
(
dest_usage_key
.
course_key
)
course
.
tabs
.
append
(
StaticTab
(
name
=
display_name
,
url_slug
=
dest_usage_key
.
name
,
)
)
store
.
update_item
(
course
,
user
.
id
)
return
created_block
cms/djangoapps/contentstore/views/item.py
View file @
1c839cc0
...
...
@@ -42,7 +42,7 @@ from student.auth import has_studio_write_access, has_studio_read_access
from
contentstore.utils
import
find_release_date_source
,
find_staff_lock_source
,
is_currently_visible_to_students
,
\
ancestor_has_staff_lock
,
has_children_visible_to_specific_content_groups
from
contentstore.views.helpers
import
is_unit
,
xblock_studio_url
,
xblock_primary_child_category
,
\
xblock_type_display_name
,
get_parent_xblock
xblock_type_display_name
,
get_parent_xblock
,
create_xblock
,
usage_key_with_run
from
contentstore.views.preview
import
get_preview_fragment
from
edxmako.shortcuts
import
render_to_string
from
models.settings.course_grading
import
CourseGradingModel
...
...
@@ -79,15 +79,6 @@ def hash_resource(resource):
return
md5
.
hexdigest
()
def
usage_key_with_run
(
usage_key_string
):
"""
Converts usage_key_string to a UsageKey, adding a course run if necessary
"""
usage_key
=
UsageKey
.
from_string
(
usage_key_string
)
usage_key
=
usage_key
.
replace
(
course_key
=
modulestore
()
.
fill_in_run
(
usage_key
.
course_key
))
return
usage_key
def
_filter_entrance_exam_grader
(
graders
):
"""
If the entrance exams feature is enabled we need to hide away the grader from
...
...
@@ -536,13 +527,12 @@ def create_item(request):
@expect_json
def
_create_item
(
request
):
"""View for create items."""
usage_key
=
usage_key_with_run
(
request
.
json
[
'parent_locator'
])
parent_locator
=
request
.
json
[
'parent_locator'
]
usage_key
=
usage_key_with_run
(
parent_locator
)
if
not
has_studio_write_access
(
request
.
user
,
usage_key
.
course_key
):
raise
PermissionDenied
()
category
=
request
.
json
[
'category'
]
display_name
=
request
.
json
.
get
(
'display_name'
)
if
isinstance
(
usage_key
,
LibraryUsageLocator
):
# Only these categories are supported at this time.
if
category
not
in
[
'html'
,
'problem'
,
'video'
]:
...
...
@@ -550,91 +540,17 @@ def _create_item(request):
"Category '
%
s' not supported for Libraries"
%
category
,
content_type
=
'text/plain'
)
store
=
modulestore
()
with
store
.
bulk_operations
(
usage_key
.
course_key
):
parent
=
store
.
get_item
(
usage_key
)
dest_usage_key
=
usage_key
.
replace
(
category
=
category
,
name
=
uuid4
()
.
hex
)
# get the metadata, display_name, and definition from the request
metadata
=
{}
data
=
None
template_id
=
request
.
json
.
get
(
'boilerplate'
)
if
template_id
:
clz
=
parent
.
runtime
.
load_block_type
(
category
)
if
clz
is
not
None
:
template
=
clz
.
get_template
(
template_id
)
if
template
is
not
None
:
metadata
=
template
.
get
(
'metadata'
,
{})
data
=
template
.
get
(
'data'
)
if
display_name
is
not
None
:
metadata
[
'display_name'
]
=
display_name
# Entrance Exams: Chapter module positioning
child_position
=
None
if
settings
.
FEATURES
.
get
(
'ENTRANCE_EXAMS'
,
False
):
is_entrance_exam
=
request
.
json
.
get
(
'is_entrance_exam'
,
False
)
if
category
==
'chapter'
and
is_entrance_exam
:
metadata
[
'is_entrance_exam'
]
=
is_entrance_exam
metadata
[
'in_entrance_exam'
]
=
True
# Inherited metadata, all children will have it
child_position
=
0
# TODO need to fix components that are sending definition_data as strings, instead of as dicts
# For now, migrate them into dicts here.
if
isinstance
(
data
,
basestring
):
data
=
{
'data'
:
data
}
created_block
=
store
.
create_child
(
request
.
user
.
id
,
usage_key
,
dest_usage_key
.
block_type
,
block_id
=
dest_usage_key
.
block_id
,
definition_data
=
data
,
metadata
=
metadata
,
runtime
=
parent
.
runtime
,
position
=
child_position
)
# Entrance Exams: Grader assignment
if
settings
.
FEATURES
.
get
(
'ENTRANCE_EXAMS'
,
False
):
course
=
store
.
get_course
(
usage_key
.
course_key
)
if
hasattr
(
course
,
'entrance_exam_enabled'
)
and
course
.
entrance_exam_enabled
:
if
category
==
'sequential'
and
request
.
json
.
get
(
'parent_locator'
)
==
course
.
entrance_exam_id
:
grader
=
{
"type"
:
"Entrance Exam"
,
"min_count"
:
0
,
"drop_count"
:
0
,
"short_label"
:
"Entrance"
,
"weight"
:
0
}
grading_model
=
CourseGradingModel
.
update_grader_from_json
(
course
.
id
,
grader
,
request
.
user
)
CourseGradingModel
.
update_section_grader_type
(
created_block
,
grading_model
[
'type'
],
request
.
user
)
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
# if we add one then we need to also add it to the policy information (i.e. metadata)
# we should remove this once we can break this reference from the course to static tabs
if
category
==
'static_tab'
:
display_name
=
display_name
or
_
(
"Empty"
)
# Prevent name being None
course
=
store
.
get_course
(
dest_usage_key
.
course_key
)
course
.
tabs
.
append
(
StaticTab
(
name
=
display_name
,
url_slug
=
dest_usage_key
.
name
,
)
)
store
.
update_item
(
course
,
request
.
user
.
id
)
return
JsonResponse
(
{
"locator"
:
unicode
(
created_block
.
location
),
"courseKey"
:
unicode
(
created_block
.
location
.
course_key
)}
)
created_block
=
create_xblock
(
parent_locator
=
parent_locator
,
user
=
request
.
user
,
category
=
category
,
display_name
=
request
.
json
.
get
(
'display_name'
),
boilerplate
=
request
.
json
.
get
(
'boilerplate'
)
)
return
JsonResponse
(
{
"locator"
:
unicode
(
created_block
.
location
),
"courseKey"
:
unicode
(
created_block
.
location
.
course_key
)}
)
def
_duplicate_item
(
parent_usage_key
,
duplicate_source_usage_key
,
user
,
display_name
=
None
):
...
...
cms/djangoapps/contentstore/views/tests/test_item.py
View file @
1c839cc0
...
...
@@ -1378,6 +1378,20 @@ 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
):
chapter
=
ItemFactory
.
create
(
parent_location
=
self
.
course
.
location
,
category
=
'chapter'
,
display_name
=
"Entrance Exam"
,
user_id
=
self
.
user
.
id
,
is_entrance_exam
=
True
)
chapter
=
modulestore
()
.
get_item
(
chapter
.
location
)
xblock_info
=
create_xblock_info
(
chapter
,
include_child_info
=
True
,
include_children_predicate
=
ALWAYS
,
)
self
.
assertEqual
(
xblock_info
[
'override_type'
],
{
'is_entrance_exam'
:
True
})
self
.
assertEqual
(
xblock_info
[
'display_name'
],
'Entrance Exam'
)
def
test_chapter_xblock_info
(
self
):
chapter
=
modulestore
()
.
get_item
(
self
.
chapter
.
location
)
xblock_info
=
create_xblock_info
(
...
...
cms/static/js/spec/models/xblock_info_spec.js
View file @
1c839cc0
...
...
@@ -7,6 +7,16 @@ 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
);
});
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
);
});
});
}
);
cms/static/js/spec/views/settings/main_spec.js
View file @
1c839cc0
...
...
@@ -3,6 +3,13 @@ define([
'js/common_helpers/ajax_helpers'
],
function
(
$
,
CourseDetailsModel
,
MainView
,
AjaxHelpers
)
{
'use strict'
;
var
SELECTORS
=
{
entrance_exam_min_score
:
'#entrance-exam-minimum-score-pct'
,
entrance_exam_enabled_field
:
'#entrance-exam-enabled'
,
grade_requirement_div
:
'.div-grade-requirements div'
};
describe
(
'Settings/Main'
,
function
()
{
var
urlRoot
=
'/course/settings/org/DemoX/Demo_Course'
,
modelData
=
{
...
...
@@ -79,12 +86,52 @@ define([
AjaxHelpers
.
respondWithJson
(
requests
,
expectedJson
);
});
it
(
'should save entrance exam course details information correctly'
,
function
()
{
var
entrance_exam_minimum_score_pct
=
'60'
;
var
entrance_exam_enabled
=
'true'
;
it
(
'should disallow save with an invalid minimum score percentage'
,
function
(){
var
entrance_exam_enabled_field
=
this
.
view
.
$
(
SELECTORS
.
entrance_exam_enabled_field
),
entrance_exam_min_score
=
this
.
view
.
$
(
SELECTORS
.
entrance_exam_min_score
);
//input some invalid values.
expect
(
entrance_exam_min_score
.
val
(
'101'
).
trigger
(
'input'
)).
toHaveClass
(
"error"
);
expect
(
entrance_exam_min_score
.
val
(
'invalidVal'
).
trigger
(
'input'
)).
toHaveClass
(
"error"
);
});
it
(
'should provide a default value for the minimum score percentage'
,
function
(){
var
entrance_exam_min_score
=
this
.
view
.
$
(
SELECTORS
.
entrance_exam_min_score
);
//if input an empty value, model should be populated with the default value.
entrance_exam_min_score
.
val
(
''
).
trigger
(
'input'
);
expect
(
this
.
model
.
get
(
'entrance_exam_minimum_score_pct'
))
.
toEqual
(
this
.
model
.
defaults
.
entrance_exam_minimum_score_pct
);
});
it
(
'show and hide the grade requirement section when the check box is selected and deselected respectively'
,
function
(){
var
entrance_exam_enabled_field
=
this
.
view
.
$
(
SELECTORS
.
entrance_exam_enabled_field
);
// select the entrance-exam-enabled checkbox. grade requirement section should be visible.
entrance_exam_enabled_field
.
attr
(
'checked'
,
'true'
)
.
trigger
(
'change'
);
this
.
view
.
render
();
expect
(
this
.
view
.
$
(
SELECTORS
.
grade_requirement_div
)).
toBeVisible
();
// deselect the entrance-exam-enabled checkbox. grade requirement section should be hidden.
entrance_exam_enabled_field
.
removeAttr
(
'checked'
)
.
trigger
(
'change'
);
expect
(
this
.
view
.
$
(
SELECTORS
.
grade_requirement_div
)).
toBeHidden
();
var
entrance_exam_min_score
=
this
.
view
.
$
(
'#entrance-exam-minimum-score-pct'
);
var
entrance_exam_enabled_field
=
this
.
view
.
$
(
'#entrance-exam-enabled'
);
});
it
(
'should save entrance exam course details information correctly'
,
function
()
{
var
entrance_exam_minimum_score_pct
=
'60'
,
entrance_exam_enabled
=
'true'
,
entrance_exam_min_score
=
this
.
view
.
$
(
SELECTORS
.
entrance_exam_min_score
),
entrance_exam_enabled_field
=
this
.
view
.
$
(
SELECTORS
.
entrance_exam_enabled_field
);
var
requests
=
AjaxHelpers
.
requests
(
this
),
expectedJson
=
$
.
extend
(
true
,
{},
modelData
,
{
...
...
@@ -92,22 +139,10 @@ define([
entrance_exam_minimum_score_pct
:
entrance_exam_minimum_score_pct
});
expect
(
this
.
view
.
$
(
'.div-grade-requirements div'
)).
toBeHidden
();
// select the entrance-exam-enabled checkbox. grade requirement section should be visible
// select the entrance-exam-enabled checkbox.
entrance_exam_enabled_field
.
attr
(
'checked'
,
entrance_exam_enabled
)
.
attr
(
'checked'
,
'true'
)
.
trigger
(
'change'
);
expect
(
this
.
view
.
$
(
'.div-grade-requirements div'
)).
toBeVisible
();
//input some invalid values.
expect
(
entrance_exam_min_score
.
val
(
'101'
).
trigger
(
'input'
)).
toHaveClass
(
"error"
);
expect
(
entrance_exam_min_score
.
val
(
'invalidVal'
).
trigger
(
'input'
)).
toHaveClass
(
"error"
);
//if input an empty value, model should be populated with the default value.
entrance_exam_min_score
.
val
(
''
).
trigger
(
'input'
);
expect
(
this
.
model
.
get
(
'entrance_exam_minimum_score_pct'
))
.
toEqual
(
this
.
model
.
defaults
.
entrance_exam_minimum_score_pct
);
// input a valid value for entrance exam minimum score.
entrance_exam_min_score
.
val
(
entrance_exam_minimum_score_pct
).
trigger
(
'input'
);
...
...
common/lib/xmodule/xmodule/seq_module.py
View file @
1c839cc0
...
...
@@ -52,7 +52,7 @@ class SequenceFields(object):
"Tag this course module as an Entrance Exam. "
+
"Note, you must enable Entrance Exams for this course setting to take effect."
),
scope
=
Scope
.
settings
,
scope
=
Scope
.
content
,
)
...
...
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