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
9cedb7cb
Commit
9cedb7cb
authored
Jul 19, 2015
by
Xavier Antoviaque
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'dropdown-pagination' into dropdown-pagination-rebased
parents
4a9a6d44
46dab9e3
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
320 additions
and
68 deletions
+320
-68
problem_builder/instructor_tool.py
+116
-6
problem_builder/public/css/instructor_tool.css
+7
-1
problem_builder/public/js/instructor_tool.js
+148
-44
problem_builder/tasks.py
+3
-12
problem_builder/templates/html/instructor_tool.html
+10
-2
problem_builder/tests/integration/test_instructor_tool.py
+36
-3
No files found.
problem_builder/instructor_tool.py
View file @
9cedb7cb
...
@@ -23,14 +23,17 @@ Instructor Tool: An XBlock for instructors to export student answers from a cour
...
@@ -23,14 +23,17 @@ Instructor Tool: An XBlock for instructors to export student answers from a cour
All processing is done offline.
All processing is done offline.
"""
"""
import
json
import
json
from
django.core.paginator
import
Paginator
from
xblock.core
import
XBlock
from
xblock.core
import
XBlock
from
xblock.exceptions
import
JsonHandlerError
from
xblock.exceptions
import
JsonHandlerError
from
xblock.fields
import
Scope
,
String
,
Dict
from
xblock.fields
import
Scope
,
String
,
Dict
,
List
from
xblock.fragment
import
Fragment
from
xblock.fragment
import
Fragment
from
xblockutils.resources
import
ResourceLoader
from
xblockutils.resources
import
ResourceLoader
loader
=
ResourceLoader
(
__name__
)
loader
=
ResourceLoader
(
__name__
)
PAGE_SIZE
=
15
# Make '_' a no-op so we can scrape strings
# Make '_' a no-op so we can scrape strings
def
_
(
text
):
def
_
(
text
):
...
@@ -63,6 +66,12 @@ class InstructorToolBlock(XBlock):
...
@@ -63,6 +66,12 @@ class InstructorToolBlock(XBlock):
default
=
None
,
default
=
None
,
scope
=
Scope
.
user_state
,
scope
=
Scope
.
user_state
,
)
)
display_data
=
List
(
# The list of results associated with the most recent successful export.
# Stored separately to avoid the overhead of sending it to the client.
default
=
None
,
scope
=
Scope
.
user_state
,
)
has_author_view
=
True
has_author_view
=
True
@property
@property
...
@@ -75,6 +84,11 @@ class InstructorToolBlock(XBlock):
...
@@ -75,6 +84,11 @@ class InstructorToolBlock(XBlock):
# different celery queues; our task listener is waiting for tasks on the LMS queue)
# different celery queues; our task listener is waiting for tasks on the LMS queue)
return
Fragment
(
u'<p>Instructor Tool Block</p><p>This block only works from the LMS.</p>'
)
return
Fragment
(
u'<p>Instructor Tool Block</p><p>This block only works from the LMS.</p>'
)
def
studio_view
(
self
,
context
=
None
):
""" View for editing Instructor Tool block in Studio. """
# Display friendly message explaining that the block is not editable.
return
Fragment
(
u'<p>This is a preconfigured block. It is not editable.</p>'
)
def
check_pending_export
(
self
):
def
check_pending_export
(
self
):
"""
"""
If we're waiting for an export, see if it has finished, and if so, get the result.
If we're waiting for an export, see if it has finished, and if so, get the result.
...
@@ -90,11 +104,26 @@ class InstructorToolBlock(XBlock):
...
@@ -90,11 +104,26 @@ class InstructorToolBlock(XBlock):
self
.
active_export_task_id
=
''
self
.
active_export_task_id
=
''
if
task_result
.
successful
():
if
task_result
.
successful
():
if
isinstance
(
task_result
.
result
,
dict
)
and
not
task_result
.
result
.
get
(
'error'
):
if
isinstance
(
task_result
.
result
,
dict
)
and
not
task_result
.
result
.
get
(
'error'
):
self
.
display_data
=
task_result
.
result
[
'display_data'
]
del
task_result
.
result
[
'display_data'
]
self
.
last_export_result
=
task_result
.
result
self
.
last_export_result
=
task_result
.
result
else
:
else
:
self
.
last_export_result
=
{
'error'
:
u'Unexpected result: {}'
.
format
(
repr
(
task_result
.
result
))}
self
.
last_export_result
=
{
'error'
:
u'Unexpected result: {}'
.
format
(
repr
(
task_result
.
result
))}
self
.
display_data
=
None
else
:
else
:
self
.
last_export_result
=
{
'error'
:
unicode
(
task_result
.
result
)}
self
.
last_export_result
=
{
'error'
:
unicode
(
task_result
.
result
)}
self
.
display_data
=
None
@XBlock.json_handler
def
get_result_page
(
self
,
data
,
suffix
=
''
):
""" Return requested page of `last_export_result`. """
paginator
=
Paginator
(
self
.
display_data
,
PAGE_SIZE
)
page
=
data
.
get
(
'page'
,
None
)
return
{
'display_data'
:
paginator
.
page
(
page
)
.
object_list
,
'num_results'
:
len
(
self
.
display_data
),
'page_size'
:
PAGE_SIZE
}
def
student_view
(
self
,
context
=
None
):
def
student_view
(
self
,
context
=
None
):
""" Normal View """
""" Normal View """
...
@@ -105,9 +134,92 @@ class InstructorToolBlock(XBlock):
...
@@ -105,9 +134,92 @@ class InstructorToolBlock(XBlock):
_
(
'Rating Question'
):
'RatingBlock'
,
_
(
'Rating Question'
):
'RatingBlock'
,
_
(
'Long Answer'
):
'AnswerBlock'
,
_
(
'Long Answer'
):
'AnswerBlock'
,
}
}
block_types
=
(
'pb-mcq'
,
'pb-rating'
,
'pb-answer'
)
flat_block_tree
=
[]
def
get_block_id
(
block
):
"""
Return ID of `block`, taking into account needs of both LMS/CMS and workbench runtimes.
"""
usage_id
=
block
.
scope_ids
.
usage_id
# Try accessing block ID. If usage_id does not have it, return usage_id itself
return
unicode
(
getattr
(
usage_id
,
'block_id'
,
usage_id
))
def
get_block_name
(
block
):
"""
Return name of `block`.
Try attributes in the following order:
- block.question
- block.name (fallback for old courses)
- block.display_name
- block ID
"""
# - Try "question" attribute:
block_name
=
getattr
(
block
,
'question'
,
block
.
name
)
if
not
block_name
:
# - Try display_name:
block_name
=
getattr
(
block
,
'display_name'
,
None
)
if
not
block_name
:
# - Default to ID:
block_name
=
get_block_id
(
block
)
return
block_name
def
get_block_type
(
block
):
"""
Return type of `block`, taking into account different key styles that might be in use.
"""
try
:
block_type
=
block
.
runtime
.
id_reader
.
get_block_type
(
block
.
scope_ids
.
def_id
)
except
AttributeError
:
block_type
=
block
.
runtime
.
id_reader
.
get_block_type
(
block
.
scope_ids
.
usage_id
)
return
block_type
def
build_tree
(
block
,
ancestors
):
"""
Build up a tree of information about the XBlocks descending from root_block
"""
block_id
=
get_block_id
(
block
)
block_name
=
get_block_name
(
block
)
block_type
=
get_block_type
(
block
)
if
not
block_type
==
'pb-choice'
:
eligible
=
block_type
in
block_types
if
eligible
:
# If this block is a question whose answers we can export,
# we mark all of its ancestors as exportable too
if
ancestors
and
not
ancestors
[
-
1
][
"eligible"
]:
for
ancestor
in
ancestors
:
ancestor
[
"eligible"
]
=
True
new_entry
=
{
"depth"
:
len
(
ancestors
),
"id"
:
block_id
,
"name"
:
block_name
,
"eligible"
:
eligible
,
}
flat_block_tree
.
append
(
new_entry
)
if
block
.
has_children
and
not
getattr
(
block
,
"has_dynamic_children"
,
lambda
:
False
)():
for
child_id
in
block
.
children
:
build_tree
(
block
.
runtime
.
get_block
(
child_id
),
ancestors
=
(
ancestors
+
[
new_entry
]))
root_block
=
self
while
root_block
.
parent
:
root_block
=
root_block
.
get_parent
()
root_block_id
=
get_block_id
(
root_block
)
root_entry
=
{
"depth"
:
0
,
"id"
:
root_block_id
,
"name"
:
"All"
,
}
flat_block_tree
.
append
(
root_entry
)
for
child_id
in
root_block
.
children
:
child_block
=
root_block
.
runtime
.
get_block
(
child_id
)
build_tree
(
child_block
,
[
root_entry
])
html
=
loader
.
render_template
(
html
=
loader
.
render_template
(
'templates/html/instructor_tool.html'
,
'templates/html/instructor_tool.html'
,
{
'block_choices'
:
block_choices
}
{
'block_choices'
:
block_choices
,
'block_tree'
:
flat_block_tree
}
)
)
fragment
=
Fragment
(
html
)
fragment
=
Fragment
(
html
)
fragment
.
add_css_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/css/instructor_tool.css'
))
fragment
.
add_css_url
(
self
.
runtime
.
local_resource_url
(
self
,
'public/css/instructor_tool.css'
))
...
@@ -144,6 +256,7 @@ class InstructorToolBlock(XBlock):
...
@@ -144,6 +256,7 @@ class InstructorToolBlock(XBlock):
self
.
last_export_result
=
{
self
.
last_export_result
=
{
'error'
:
message
,
'error'
:
message
,
}
}
self
.
display_data
=
None
raise
JsonHandlerError
(
code
,
message
)
raise
JsonHandlerError
(
code
,
message
)
@XBlock.json_handler
@XBlock.json_handler
...
@@ -157,6 +270,7 @@ class InstructorToolBlock(XBlock):
...
@@ -157,6 +270,7 @@ class InstructorToolBlock(XBlock):
def
_delete_export
(
self
):
def
_delete_export
(
self
):
self
.
last_export_result
=
None
self
.
last_export_result
=
None
self
.
display_data
=
None
self
.
active_export_task_id
=
''
self
.
active_export_task_id
=
''
@XBlock.json_handler
@XBlock.json_handler
...
@@ -187,9 +301,6 @@ class InstructorToolBlock(XBlock):
...
@@ -187,9 +301,6 @@ class InstructorToolBlock(XBlock):
root_block_id
=
self
.
scope_ids
.
usage_id
root_block_id
=
self
.
scope_ids
.
usage_id
# Block ID not in workbench runtime.
# Block ID not in workbench runtime.
root_block_id
=
unicode
(
getattr
(
root_block_id
,
'block_id'
,
root_block_id
))
root_block_id
=
unicode
(
getattr
(
root_block_id
,
'block_id'
,
root_block_id
))
get_root
=
True
else
:
get_root
=
False
# Launch task
# Launch task
from
.tasks
import
export_data
as
export_data_task
# Import here since this is edX LMS specific
from
.tasks
import
export_data
as
export_data_task
# Import here since this is edX LMS specific
...
@@ -203,7 +314,6 @@ class InstructorToolBlock(XBlock):
...
@@ -203,7 +314,6 @@ class InstructorToolBlock(XBlock):
block_types
,
block_types
,
user_id
,
user_id
,
match_string
,
match_string
,
get_root
=
get_root
)
)
if
async_result
.
ready
():
if
async_result
.
ready
():
# In development mode, the task may have executed synchronously.
# In development mode, the task may have executed synchronously.
...
...
problem_builder/public/css/instructor_tool.css
View file @
9cedb7cb
...
@@ -25,6 +25,12 @@
...
@@ -25,6 +25,12 @@
display
:
table-cell
;
display
:
table-cell
;
padding-left
:
1em
;
padding-left
:
1em
;
}
}
.data-export-field-container
{
width
:
43%
;
}
.data-export-options
.data-export-actions
{
max-width
:
10%
;
}
.data-export-field
{
.data-export-field
{
margin-top
:
.5em
;
margin-top
:
.5em
;
margin-bottom
:
.5em
;
margin-bottom
:
.5em
;
...
@@ -34,7 +40,7 @@
...
@@ -34,7 +40,7 @@
vertical-align
:
middle
;
vertical-align
:
middle
;
}
}
.data-export-field
input
,
.data-export-field
select
{
.data-export-field
input
,
.data-export-field
select
{
max-width
:
60
%
;
width
:
55
%
;
float
:
right
;
float
:
right
;
}
}
.data-export-results
,
.data-export-download
,
.data-export-cancel
,
.data-export-delete
{
.data-export-results
,
.data-export-download
,
.data-export-cancel
,
.data-export-delete
{
...
...
problem_builder/public/js/instructor_tool.js
View file @
9cedb7cb
This diff is collapsed.
Click to expand it.
problem_builder/tasks.py
View file @
9cedb7cb
...
@@ -6,7 +6,6 @@ import time
...
@@ -6,7 +6,6 @@ import time
from
celery.task
import
task
from
celery.task
import
task
from
celery.utils.log
import
get_task_logger
from
celery.utils.log
import
get_task_logger
from
instructor_task.models
import
ReportStore
from
instructor_task.models
import
ReportStore
from
opaque_keys
import
InvalidKeyError
from
opaque_keys.edx.keys
import
CourseKey
from
opaque_keys.edx.keys
import
CourseKey
from
student.models
import
user_by_anonymous_id
from
student.models
import
user_by_anonymous_id
from
xmodule.modulestore.django
import
modulestore
from
xmodule.modulestore.django
import
modulestore
...
@@ -21,7 +20,7 @@ logger = get_task_logger(__name__)
...
@@ -21,7 +20,7 @@ logger = get_task_logger(__name__)
@task
()
@task
()
def
export_data
(
course_id
,
source_block_id_str
,
block_types
,
user_id
,
match_string
,
get_root
=
True
):
def
export_data
(
course_id
,
source_block_id_str
,
block_types
,
user_id
,
match_string
):
"""
"""
Exports student answers to all MCQ questions to a CSV file.
Exports student answers to all MCQ questions to a CSV file.
"""
"""
...
@@ -31,18 +30,10 @@ def export_data(course_id, source_block_id_str, block_types, user_id, match_stri
...
@@ -31,18 +30,10 @@ def export_data(course_id, source_block_id_str, block_types, user_id, match_stri
try
:
try
:
course_key
=
CourseKey
.
from_string
(
course_id
)
course_key
=
CourseKey
.
from_string
(
course_id
)
src_block
=
modulestore
()
.
get_items
(
course_key
,
qualifiers
=
{
'name'
:
source_block_id_str
},
depth
=
0
)[
0
]
src_block
=
modulestore
()
.
get_items
(
course_key
,
qualifiers
=
{
'name'
:
source_block_id_str
},
depth
=
0
)[
0
]
if
src_block
is
None
:
except
IndexError
:
raise
InvalidKeyError
except
InvalidKeyError
:
raise
ValueError
(
"Could not find the specified Block ID."
)
raise
ValueError
(
"Could not find the specified Block ID."
)
course_key_str
=
unicode
(
course_key
)
course_key_str
=
unicode
(
course_key
)
root
=
src_block
if
get_root
:
# Get the root block for the course.
while
root
.
parent
:
root
=
root
.
get_parent
()
type_map
=
{
cls
.
__name__
:
cls
for
cls
in
[
MCQBlock
,
RatingBlock
,
AnswerBlock
]}
type_map
=
{
cls
.
__name__
:
cls
for
cls
in
[
MCQBlock
,
RatingBlock
,
AnswerBlock
]}
if
not
block_types
:
if
not
block_types
:
...
@@ -65,7 +56,7 @@ def export_data(course_id, source_block_id_str, block_types, user_id, match_stri
...
@@ -65,7 +56,7 @@ def export_data(course_id, source_block_id_str, block_types, user_id, match_stri
# Blocks may refer to missing children. Don't break in this case.
# Blocks may refer to missing children. Don't break in this case.
pass
pass
scan_for_blocks
(
root
)
scan_for_blocks
(
src_block
)
# Define the header row of our CSV:
# Define the header row of our CSV:
rows
=
[]
rows
=
[]
...
...
problem_builder/templates/html/instructor_tool.html
View file @
9cedb7cb
...
@@ -27,8 +27,16 @@
...
@@ -27,8 +27,16 @@
<div
class=
"data-export-field-container"
>
<div
class=
"data-export-field-container"
>
<div
class=
"data-export-field"
>
<div
class=
"data-export-field"
>
<label>
<label>
<span>
{% trans "Root block ID:" %}
</span>
<span>
{% trans "Section/Question:" %}
</span>
<input
type=
"text"
name=
"root_block_id"
/>
<select
name=
"root_block_id"
>
{% for block in block_tree %}
<option
value=
"{{ block.id }}"
{%
if
not
block
.
eligible
%}
disabled=
"disabled"
{%
endif
%}
>
{% for _ in ""|ljust:block.depth %}
{% endfor %}
{{ block.name }}
</option>
{% endfor %}
</select>
</label>
</label>
</div>
</div>
</div>
</div>
...
...
problem_builder/tests/integration/test_instructor_tool.py
View file @
9cedb7cb
...
@@ -7,7 +7,7 @@ from mock import patch, Mock
...
@@ -7,7 +7,7 @@ from mock import patch, Mock
from
selenium.common.exceptions
import
NoSuchElementException
from
selenium.common.exceptions
import
NoSuchElementException
from
xblockutils.base_test
import
SeleniumXBlockTest
from
xblockutils.base_test
import
SeleniumXBlockTest
from
problem_builder.instructor_tool
import
InstructorToolBlock
from
problem_builder.instructor_tool
import
PAGE_SIZE
,
InstructorToolBlock
class
MockTasksModule
(
object
):
class
MockTasksModule
(
object
):
...
@@ -62,6 +62,36 @@ class InstructorToolTest(SeleniumXBlockTest):
...
@@ -62,6 +62,36 @@ 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_data_export_delete
(
self
):
instructor_tool
=
self
.
go_to_view
()
start_button
=
instructor_tool
.
find_element_by_class_name
(
'data-export-start'
)
result_block
=
instructor_tool
.
find_element_by_class_name
(
'data-export-results'
)
status_area
=
instructor_tool
.
find_element_by_class_name
(
'data-export-status'
)
download_button
=
instructor_tool
.
find_element_by_class_name
(
'data-export-download'
)
cancel_button
=
instructor_tool
.
find_element_by_class_name
(
'data-export-cancel'
)
delete_button
=
instructor_tool
.
find_element_by_class_name
(
'data-export-delete'
)
start_button
.
click
()
self
.
wait_until_visible
(
result_block
)
self
.
wait_until_visible
(
delete_button
)
delete_button
.
click
()
self
.
wait_until_hidden
(
result_block
)
self
.
wait_until_hidden
(
delete_button
)
self
.
assertTrue
(
start_button
.
is_enabled
())
self
.
assertEqual
(
''
,
status_area
.
text
)
self
.
assertFalse
(
download_button
.
is_displayed
())
self
.
assertFalse
(
cancel_button
.
is_displayed
())
@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_success
(
self
):
def
test_data_export_success
(
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'
)
...
@@ -146,6 +176,7 @@ class InstructorToolTest(SeleniumXBlockTest):
...
@@ -146,6 +176,7 @@ class InstructorToolTest(SeleniumXBlockTest):
start_button
.
click
()
start_button
.
click
()
self
.
wait_until_visible
(
result_block
)
self
.
wait_until_visible
(
result_block
)
time
.
sleep
(
1
)
# Allow some time for result block to fully fade in
self
.
assertFalse
(
first_page_button
.
is_enabled
())
self
.
assertFalse
(
first_page_button
.
is_enabled
())
self
.
assertFalse
(
prev_page_button
.
is_enabled
())
self
.
assertFalse
(
prev_page_button
.
is_enabled
())
...
@@ -179,6 +210,7 @@ class InstructorToolTest(SeleniumXBlockTest):
...
@@ -179,6 +210,7 @@ class InstructorToolTest(SeleniumXBlockTest):
start_button
.
click
()
start_button
.
click
()
self
.
wait_until_visible
(
result_block
)
self
.
wait_until_visible
(
result_block
)
time
.
sleep
(
1
)
# Allow some time for result block to fully fade in
for
contents
in
[
for
contents
in
[
'Test section'
,
'Test subsection'
,
'Test unit'
,
'Test section'
,
'Test subsection'
,
'Test unit'
,
...
@@ -199,7 +231,7 @@ class InstructorToolTest(SeleniumXBlockTest):
...
@@ -199,7 +231,7 @@ class InstructorToolTest(SeleniumXBlockTest):
successful
=
True
,
display_data
=
[[
successful
=
True
,
display_data
=
[[
'Test section'
,
'Test subsection'
,
'Test unit'
,
'Test section'
,
'Test subsection'
,
'Test unit'
,
'Test type'
,
'Test question'
,
'Test answer'
,
'Test username'
'Test type'
,
'Test question'
,
'Test answer'
,
'Test username'
]
for
_
in
range
(
45
)]),
]
for
_
in
range
(
PAGE_SIZE
*
3
)]),
'instructor_task'
:
True
,
'instructor_task'
:
True
,
'instructor_task.models'
:
MockInstructorTaskModelsModule
(),
'instructor_task.models'
:
MockInstructorTaskModelsModule
(),
})
})
...
@@ -218,13 +250,14 @@ class InstructorToolTest(SeleniumXBlockTest):
...
@@ -218,13 +250,14 @@ class InstructorToolTest(SeleniumXBlockTest):
start_button
.
click
()
start_button
.
click
()
self
.
wait_until_visible
(
result_block
)
self
.
wait_until_visible
(
result_block
)
time
.
sleep
(
1
)
# Allow some time for result block to fully fade in
for
contents
in
[
for
contents
in
[
'Test section'
,
'Test subsection'
,
'Test unit'
,
'Test section'
,
'Test subsection'
,
'Test unit'
,
'Test type'
,
'Test question'
,
'Test answer'
,
'Test username'
'Test type'
,
'Test question'
,
'Test answer'
,
'Test username'
]:
]:
occurrences
=
re
.
findall
(
contents
,
result_block
.
text
)
occurrences
=
re
.
findall
(
contents
,
result_block
.
text
)
self
.
assertEqual
(
len
(
occurrences
),
15
)
self
.
assertEqual
(
len
(
occurrences
),
PAGE_SIZE
)
self
.
assertFalse
(
first_page_button
.
is_enabled
())
self
.
assertFalse
(
first_page_button
.
is_enabled
())
self
.
assertFalse
(
prev_page_button
.
is_enabled
())
self
.
assertFalse
(
prev_page_button
.
is_enabled
())
...
...
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