Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
problem-builder
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
OpenEdx
problem-builder
Commits
a9f7743d
Commit
a9f7743d
authored
Aug 10, 2015
by
Tim Krones
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #50 from open-craft/instructor-tool-unit-tests
Add unit tests for instructor tool
parents
36cd10b9
66e67621
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
316 additions
and
13 deletions
+316
-13
problem_builder/instructor_tool.py
+23
-13
problem_builder/tests/integration/test_instructor_tool.py
+38
-0
problem_builder/tests/unit/test_instructor_tool.py
+255
-0
No files found.
problem_builder/instructor_tool.py
View file @
a9f7743d
...
@@ -134,6 +134,26 @@ class InstructorToolBlock(XBlock):
...
@@ -134,6 +134,26 @@ class InstructorToolBlock(XBlock):
_
(
'Rating Question'
):
'RatingBlock'
,
_
(
'Rating Question'
):
'RatingBlock'
,
_
(
'Long Answer'
):
'AnswerBlock'
,
_
(
'Long Answer'
):
'AnswerBlock'
,
}
}
flat_block_tree
=
self
.
_build_course_tree
()
html
=
loader
.
render_template
(
'templates/html/instructor_tool.html'
,
{
'block_choices'
:
block_choices
,
'block_tree'
:
flat_block_tree
}
)
fragment
=
Fragment
(
html
)
fragment
.
add_css_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/css/instructor_tool.css'
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/js/instructor_tool.js'
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/js/vendor/underscore-min.js'
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/js/vendor/backbone-min.js'
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/js/vendor/backbone.paginator.min.js'
))
fragment
.
initialize_js
(
'InstructorToolBlock'
)
return
fragment
def
_build_course_tree
(
self
):
"""
Return flat tree of blocks belonging to this block's parent course.
"""
eligible_block_types
=
(
'pb-mcq'
,
'pb-rating'
,
'pb-answer'
)
eligible_block_types
=
(
'pb-mcq'
,
'pb-rating'
,
'pb-answer'
)
flat_block_tree
=
[]
flat_block_tree
=
[]
...
@@ -172,7 +192,7 @@ class InstructorToolBlock(XBlock):
...
@@ -172,7 +192,7 @@ class InstructorToolBlock(XBlock):
def
build_tree
(
block
,
ancestors
):
def
build_tree
(
block
,
ancestors
):
"""
"""
Build up a tree of information about the XBlocks descending from
root_block
Build up a tree of information about the XBlocks descending from
`block`.
"""
"""
block_id
=
get_block_id
(
block
)
block_id
=
get_block_id
(
block
)
block_name
=
get_block_name
(
block
)
block_name
=
get_block_name
(
block
)
...
@@ -205,6 +225,7 @@ class InstructorToolBlock(XBlock):
...
@@ -205,6 +225,7 @@ class InstructorToolBlock(XBlock):
"depth"
:
0
,
"depth"
:
0
,
"id"
:
root_block_id
,
"id"
:
root_block_id
,
"name"
:
"All"
,
"name"
:
"All"
,
"eligible"
:
False
,
}
}
flat_block_tree
.
append
(
root_entry
)
flat_block_tree
.
append
(
root_entry
)
...
@@ -212,18 +233,7 @@ class InstructorToolBlock(XBlock):
...
@@ -212,18 +233,7 @@ class InstructorToolBlock(XBlock):
child_block
=
root_block
.
runtime
.
get_block
(
child_id
)
child_block
=
root_block
.
runtime
.
get_block
(
child_id
)
build_tree
(
child_block
,
[
root_entry
])
build_tree
(
child_block
,
[
root_entry
])
html
=
loader
.
render_template
(
return
flat_block_tree
'templates/html/instructor_tool.html'
,
{
'block_choices'
:
block_choices
,
'block_tree'
:
flat_block_tree
}
)
fragment
=
Fragment
(
html
)
fragment
.
add_css_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/css/instructor_tool.css'
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/js/instructor_tool.js'
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/js/vendor/underscore-min.js'
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/js/vendor/backbone-min.js'
))
fragment
.
add_javascript_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/js/vendor/backbone.paginator.min.js'
))
fragment
.
initialize_js
(
'InstructorToolBlock'
)
return
fragment
@property
@property
def
download_url_for_last_report
(
self
):
def
download_url_for_last_report
(
self
):
...
...
problem_builder/tests/integration/test_instructor_tool.py
View file @
a9f7743d
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
import
math
import
re
import
re
import
time
import
time
...
@@ -49,6 +50,9 @@ class InstructorToolTest(SeleniumXBlockTest):
...
@@ -49,6 +50,9 @@ class InstructorToolTest(SeleniumXBlockTest):
self
.
set_scenario_xml
(
"""
self
.
set_scenario_xml
(
"""
<vertical_demo>
<vertical_demo>
<pb-instructor-tool url_name="data_export"/>
<pb-instructor-tool url_name="data_export"/>
<problem-builder mode="standard">
<pb-answer name="answer" question="Is this a long long long long long long long long long long question?" />
</problem-builder>
</vertical_demo>
</vertical_demo>
"""
)
"""
)
...
@@ -62,6 +66,40 @@ class InstructorToolTest(SeleniumXBlockTest):
...
@@ -62,6 +66,40 @@ class InstructorToolTest(SeleniumXBlockTest):
'instructor_task.models'
:
MockInstructorTaskModelsModule
(),
'instructor_task.models'
:
MockInstructorTaskModelsModule
(),
})
})
@patch.object
(
InstructorToolBlock
,
'user_is_staff'
,
Mock
(
return_value
=
True
))
@patch.object
(
InstructorToolBlock
,
'user_is_staff'
,
Mock
(
return_value
=
True
))
def
test_export_field_container_width
(
self
):
instructor_tool
=
self
.
go_to_view
()
export_field_container
=
instructor_tool
.
find_element_by_class_name
(
'data-export-field-container'
)
parent_div
=
export_field_container
.
find_element_by_xpath
(
'..'
)
export_field_container_width
=
export_field_container
.
size
[
'width'
]
parent_div_width
=
parent_div
.
size
[
'width'
]
self
.
assertTrue
(
export_field_container_width
<=
math
.
ceil
(
0.43
*
parent_div_width
))
@patch.dict
(
'sys.modules'
,
{
'problem_builder.tasks'
:
MockTasksModule
(
successful
=
True
),
'instructor_task'
:
True
,
'instructor_task.models'
:
MockInstructorTaskModelsModule
(),
})
@patch.object
(
InstructorToolBlock
,
'user_is_staff'
,
Mock
(
return_value
=
True
))
def
test_root_block_select_width
(
self
):
instructor_tool
=
self
.
go_to_view
()
root_block_select
=
instructor_tool
.
find_element_by_name
(
'root_block_id'
)
parent_div
=
root_block_select
.
find_element_by_xpath
(
'../..'
)
root_block_select_width
=
root_block_select
.
size
[
'width'
]
parent_div_width
=
parent_div
.
size
[
'width'
]
self
.
assertTrue
(
root_block_select_width
<=
math
.
ceil
(
0.55
*
parent_div_width
))
@patch.dict
(
'sys.modules'
,
{
'problem_builder.tasks'
:
MockTasksModule
(
successful
=
True
),
'instructor_task'
:
True
,
'instructor_task.models'
:
MockInstructorTaskModelsModule
(),
})
@patch.object
(
InstructorToolBlock
,
'user_is_staff'
,
Mock
(
return_value
=
True
))
def
test_data_export_delete
(
self
):
def
test_data_export_delete
(
self
):
instructor_tool
=
self
.
go_to_view
()
instructor_tool
=
self
.
go_to_view
()
start_button
=
instructor_tool
.
find_element_by_class_name
(
'data-export-start'
)
start_button
=
instructor_tool
.
find_element_by_class_name
(
'data-export-start'
)
...
...
problem_builder/tests/unit/test_instructor_tool.py
0 → 100644
View file @
a9f7743d
"""
Unit tests for Instructor Tool block
"""
import
ddt
import
unittest
from
mock
import
Mock
,
patch
from
xblock.field_data
import
DictFieldData
from
problem_builder
import
InstructorToolBlock
@ddt.ddt
class
TestInstructorToolBlock
(
unittest
.
TestCase
):
"""
Test InstructorToolBlock with some mocked data.
"""
def
_get_block
(
self
,
block_info
):
# Build spec
spec
=
[
'scope_ids'
,
'runtime'
,
'has_children'
,
'children'
]
for
attr
in
block_info
.
get
(
'attrs'
,
[]):
spec
.
append
(
attr
)
# Assemble block
block
=
Mock
(
spec
=
spec
)
scope_ids_mock
=
Mock
()
scope_ids_mock
.
usage_id
=
block_info
.
get
(
'usage_id'
)
block
.
scope_ids
=
scope_ids_mock
block
.
runtime
=
self
.
runtime_mock
block
.
children
=
[]
for
attr
,
val
in
block_info
.
get
(
'attrs'
,
{})
.
items
():
setattr
(
block
,
attr
,
val
)
return
block
def
setUp
(
self
):
self
.
runtime_mock
=
Mock
()
self
.
runtime_mock
.
get_block
=
self
.
_get_block
scope_ids_mock
=
Mock
()
scope_ids_mock
.
usage_id
=
u'0'
self
.
block
=
InstructorToolBlock
(
self
.
runtime_mock
,
field_data
=
DictFieldData
({}),
scope_ids
=
scope_ids_mock
)
self
.
block
.
children
=
[
# No attributes: Prefer usage_id
{
'usage_id'
:
u'1'
},
# Single attribute: Prefer attribute that's present
{
'usage_id'
:
u'2'
,
'preferred_attr'
:
'question'
,
'attrs'
:
{
'question'
:
'question'
}},
{
'usage_id'
:
u'3'
,
'preferred_attr'
:
'name'
,
'attrs'
:
{
'name'
:
'name'
}},
{
'usage_id'
:
u'4'
,
'preferred_attr'
:
'display_name'
,
'attrs'
:
{
'display_name'
:
'display_name'
}},
# Two attributes (question, name): Prefer question
{
'usage_id'
:
u'5'
,
'preferred_attr'
:
'question'
,
'attrs'
:
{
'question'
:
'question'
,
'name'
:
'name'
}
},
# Two attributes (question, display_name): Prefer question
{
'usage_id'
:
u'6'
,
'preferred_attr'
:
'question'
,
'attrs'
:
{
'question'
:
'question'
,
'display_name'
:
'display_name'
}
},
# Two attributes (name, display_name): Prefer name
{
'usage_id'
:
u'7'
,
'preferred_attr'
:
'name'
,
'attrs'
:
{
'name'
:
'name'
,
'display_name'
:
'display_name'
}
},
# All attributes: Prefer question
{
'usage_id'
:
u'8'
,
'preferred_attr'
:
'question'
,
'attrs'
:
{
'question'
:
'question'
,
'name'
:
'name'
,
'display_name'
:
'display_name'
}
},
]
def
test_build_course_tree_uses_preferred_attrs
(
self
):
"""
Check if `_build_course_tree` method uses preferred block
attributes for `id` and `name` of each block.
Each entry of the block tree returned by `_build_course_tree`
is a dictionary that must contain an `id` key and a `name`
key.
- `id` must be set to the ID (usage_id or block_id)
of the corresponding block.
- `name` must be set to the value of one of the following attributes
of the corresponding block:
- question
- name (question ID)
- display_name (question title)
- block ID
Note that the attributes are listed in order of preference;
i.e., if `block.question` has a meaningful value, that value
should be used for `name` (irrespective of what the values
of the other attributes might be).
"""
block_tree
=
self
.
block
.
_build_course_tree
()
def
check_block
(
usage_id
,
expected_name
):
# - Does block_tree contain single entry whose `id` matches `usage_id` of block?
matching_blocks
=
[
block
for
block
in
block_tree
if
block
[
'id'
]
==
usage_id
]
self
.
assertEqual
(
len
(
matching_blocks
),
1
)
# - Is `name` of that entry set to `expected_name`?
matching_block
=
matching_blocks
[
0
]
self
.
assertEqual
(
matching_block
[
'name'
],
expected_name
)
# Check size of block_tree
num_blocks
=
len
(
self
.
block
.
children
)
+
1
self
.
assertEqual
(
len
(
block_tree
),
num_blocks
)
# Check block_tree for root entry
check_block
(
usage_id
=
self
.
block
.
scope_ids
.
usage_id
,
expected_name
=
'All'
)
# Check block_tree for children
for
child
in
self
.
block
.
children
:
usage_id
=
child
.
get
(
'usage_id'
)
attrs
=
child
.
get
(
'attrs'
,
{})
if
not
attrs
:
expected_name
=
usage_id
else
:
preferred_attr
=
child
.
get
(
'preferred_attr'
)
expected_name
=
attrs
[
preferred_attr
]
check_block
(
usage_id
,
expected_name
)
def
test_build_course_tree_excludes_choice_blocks
(
self
):
"""
Check if `_build_course_tree` method excludes 'pb-choice' blocks.
"""
# Pretend that all blocks in self.block.children are of type 'pb-choice:
self
.
runtime_mock
.
id_reader
=
Mock
()
self
.
runtime_mock
.
id_reader
.
get_block_type
.
return_value
=
'pb-choice'
block_tree
=
self
.
block
.
_build_course_tree
()
# Check size of block_tree: Should only include root block
self
.
assertEqual
(
len
(
block_tree
),
1
)
@ddt.data
(
'pb-mcq'
,
'pb-rating'
,
'pb-answer'
)
def
test_build_course_tree_eligible_blocks
(
self
,
block_type
):
"""
Check if `_build_course_tree` method correctly marks MCQ, Rating,
and Answer blocks as eligible.
A block is eligible if its type is one of {'pb-mcq', 'pb-rating', 'pb-answer'}.
"""
# Pretend that all blocks in self.block.children are eligible:
self
.
runtime_mock
.
id_reader
=
Mock
()
self
.
runtime_mock
.
id_reader
.
get_block_type
.
return_value
=
block_type
block_tree
=
self
.
block
.
_build_course_tree
()
# Check size of block_tree: All blocks should be included
num_blocks
=
len
(
self
.
block
.
children
)
+
1
self
.
assertEqual
(
len
(
block_tree
),
num_blocks
)
# Check if all blocks are eligible:
self
.
assertTrue
(
all
(
block
[
'eligible'
]
for
block
in
block_tree
))
@ddt.data
(
'problem-builder'
,
'pb-table'
,
'pb-column'
,
'pb-answer-recap'
,
'pb-mrq'
,
'pb-message'
,
'pb-tip'
,
'pb-dashboard'
,
'pb-data-export'
,
'pb-instructor-tool'
,
)
def
test_build_course_tree_ineligible_blocks
(
self
,
block_type
):
"""
Check if `_build_course_tree` method correctly marks blocks that
aren't MCQ, Rating, or Answer blocks as ineligible.
"""
# Pretend that none of the blocks in self.block.children are eligible:
self
.
runtime_mock
.
id_reader
=
Mock
()
self
.
runtime_mock
.
id_reader
.
get_block_type
.
return_value
=
block_type
block_tree
=
self
.
block
.
_build_course_tree
()
# Check size of block_tree: All blocks should be included (they are not of type 'pb-choice')
num_blocks
=
len
(
self
.
block
.
children
)
+
1
self
.
assertEqual
(
len
(
block_tree
),
num_blocks
)
# Check if all blocks are ineligible:
self
.
assertTrue
(
all
(
not
block
[
'eligible'
]
for
block
in
block_tree
))
def
test_build_course_tree_supports_new_style_keys
(
self
):
"""
Check if `_build_course_tree` method correctly handles new-style keys.
To determine eligibility of a given block,
`_build_course_tree` has to obtain the blocks's type. It uses
`block.runtime.id_reader.get_block_type` to do this. It first
tries to pass `block.scope_ids.def_id` as an argument.
**If old-style keys are enabled, this will work. If new-style
keys are enabled, this will fail with an AttributeError.**
`_build_course_tree` should not let this error bubble up.
Instead, it should catch the error and try again, this time
passing `block.scope_ids.usage_id` to the method mentioned
above (which will work if new-style keys are enabled).
"""
# Pretend that new-style keys are enabled:
self
.
block
.
scope_ids
.
def_id
=
Mock
()
self
.
block
.
scope_ids
.
def_id
.
block_type
.
side_effect
=
AttributeError
()
try
:
self
.
block
.
_build_course_tree
()
except
AttributeError
:
self
.
fail
(
'student_view breaks if new-style keys are enabled.'
)
def
test_student_view_template_args
(
self
):
"""
Check if `student_view` calls rendering method of template loader
with correct arguments.
"""
block_choices
=
{
'Multiple Choice Question'
:
'MCQBlock'
,
'Rating Question'
:
'RatingBlock'
,
'Long Answer'
:
'AnswerBlock'
,
}
flat_block_tree
=
[
'block{}'
.
format
(
i
)
for
i
in
range
(
10
)]
self
.
block
.
_build_course_tree
=
Mock
(
return_value
=
flat_block_tree
)
with
patch
(
'problem_builder.instructor_tool.loader'
)
as
patched_loader
:
patched_loader
.
render_template
.
return_value
=
u''
self
.
block
.
student_view
()
patched_loader
.
render_template
.
assert_called_once_with
(
'templates/html/instructor_tool.html'
,
{
'block_choices'
:
block_choices
,
'block_tree'
:
flat_block_tree
}
)
def
test_author_view
(
self
):
"""
Check if author view shows appropriate message when viewing units
containing Instructor Tool block in Studio.
"""
fragment
=
self
.
block
.
author_view
()
self
.
assertIn
(
'This block only works from the LMS.'
,
fragment
.
content
)
def
test_studio_view
(
self
):
"""
Check if studio view is present and shows appropriate message when
trying to edit Instructor Tool block in Studio.
"""
try
:
fragment
=
self
.
block
.
studio_view
()
except
AttributeError
:
self
.
fail
(
'Studio view not defined.'
)
self
.
assertIn
(
'This is a preconfigured block. It is not editable.'
,
fragment
.
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